// Copyright 2023 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 python

import (
	"path/filepath"

	"github.com/bazelbuild/bazel-gazelle/config"
	"github.com/bazelbuild/bazel-gazelle/rule"
	"github.com/emirpasic/gods/sets/treeset"
	godsutils "github.com/emirpasic/gods/utils"
)

// targetBuilder builds targets to be generated by Gazelle.
type targetBuilder struct {
	kind                  string
	name                  string
	pythonProjectRoot     string
	bzlPackage            string
	srcs                  *treeset.Set
	siblingSrcs           *treeset.Set
	deps                  *treeset.Set
	resolvedDeps          *treeset.Set
	visibility            *treeset.Set
	main                  *string
	imports               []string
	testonly              bool
	annotations           *annotations
	resolveSiblingImports bool
}

// newTargetBuilder constructs a new targetBuilder.
func newTargetBuilder(kind, name, pythonProjectRoot, bzlPackage string, siblingSrcs *treeset.Set, resolveSiblingImports bool) *targetBuilder {
	return &targetBuilder{
		kind:                  kind,
		name:                  name,
		pythonProjectRoot:     pythonProjectRoot,
		bzlPackage:            bzlPackage,
		srcs:                  treeset.NewWith(godsutils.StringComparator),
		siblingSrcs:           siblingSrcs,
		deps:                  treeset.NewWith(moduleComparator),
		resolvedDeps:          treeset.NewWith(godsutils.StringComparator),
		visibility:            treeset.NewWith(godsutils.StringComparator),
		annotations:           new(annotations),
		resolveSiblingImports: resolveSiblingImports,
	}
}

// addSrc adds a single src to the target.
func (t *targetBuilder) addSrc(src string) *targetBuilder {
	t.srcs.Add(src)
	return t
}

// addSrcs copies all values from the provided srcs to the target.
func (t *targetBuilder) addSrcs(srcs *treeset.Set) *targetBuilder {
	it := srcs.Iterator()
	for it.Next() {
		t.srcs.Add(it.Value().(string))
	}
	return t
}

// addModuleDependency adds a single module dep to the target.
func (t *targetBuilder) addModuleDependency(dep Module) *targetBuilder {
	fileName := dep.Name + ".py"
	if dep.From != "" {
		fileName = dep.From + ".py"
	}
	if t.resolveSiblingImports && t.siblingSrcs.Contains(fileName) && fileName != filepath.Base(dep.Filepath) {
		// importing another module from the same package, converting to absolute imports to make
		// dependency resolution easier
		dep.Name = importSpecFromSrc(t.pythonProjectRoot, t.bzlPackage, fileName).Imp
	}

	addModuleToTreeSet(t.deps, dep)
	return t
}

// addModuleDependencies copies all values from the provided deps to the target.
func (t *targetBuilder) addModuleDependencies(deps *treeset.Set) *targetBuilder {
	it := deps.Iterator()
	for it.Next() {
		t.addModuleDependency(it.Value().(Module))
	}
	return t
}

// addResolvedDependency adds a single dependency the target that has already
// been resolved or generated. The Resolver step doesn't process it further.
func (t *targetBuilder) addResolvedDependency(dep string) *targetBuilder {
	t.resolvedDeps.Add(dep)
	return t
}

// addResolvedDependencies adds multiple dependencies, that have already been
// resolved or generated, to the target.
func (t *targetBuilder) addResolvedDependencies(deps []string) *targetBuilder {
	for _, dep := range deps {
		t.addResolvedDependency(dep)
	}
	return t
}

// addVisibility adds visibility labels to the target.
func (t *targetBuilder) addVisibility(visibility []string) *targetBuilder {
	for _, item := range visibility {
		t.visibility.Add(item)
	}
	return t
}

// setMain sets the main file to the target.
func (t *targetBuilder) setMain(main string) *targetBuilder {
	t.main = &main
	return t
}

// setTestonly sets the testonly attribute to true.
func (t *targetBuilder) setTestonly() *targetBuilder {
	t.testonly = true
	return t
}

// setAnnotations sets the annotations attribute on the target.
func (t *targetBuilder) setAnnotations(val annotations) *targetBuilder {
	t.annotations = &val
	return t
}

// generateImportsAttribute generates the imports attribute.
// These are a list of import directories to be added to the PYTHONPATH. In our
// case, the value we add is on Bazel sub-packages to be able to perform imports
// relative to the root project package.
func (t *targetBuilder) generateImportsAttribute() *targetBuilder {
	if t.pythonProjectRoot == "" {
		// When gazelle:python_root is not set or is at the root of the repo, we don't need
		// to set imports, because that's the Bazel's default.
		return t
	}
	p, _ := filepath.Rel(t.bzlPackage, t.pythonProjectRoot)
	p = filepath.Clean(p)
	if p == "." {
		return t
	}
	t.imports = []string{p}
	return t
}

// build returns the assembled *rule.Rule for the target.
func (t *targetBuilder) build() *rule.Rule {
	r := rule.NewRule(t.kind, t.name)
	if !t.srcs.Empty() {
		r.SetAttr("srcs", t.srcs.Values())
	}
	if !t.visibility.Empty() {
		r.SetAttr("visibility", t.visibility.Values())
	}
	if t.main != nil {
		r.SetAttr("main", *t.main)
	}
	if t.imports != nil {
		r.SetAttr("imports", t.imports)
	}
	if !t.deps.Empty() {
		r.SetPrivateAttr(config.GazelleImportsKey, t.deps)
	}
	if t.testonly {
		r.SetAttr("testonly", true)
	}
	r.SetPrivateAttr(resolvedDepsKey, t.resolvedDeps)
	return r
}
