/* Copyright 2016 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 label provides utilities for parsing and manipulating
// Bazel labels. See
// https://docs.bazel.build/versions/master/build-ref.html#labels
// for more information.
package label

import (
	"fmt"
	"log"
	"path"
	"regexp"
	"strings"

	"github.com/bazelbuild/bazel-gazelle/pathtools"
	bzl "github.com/bazelbuild/buildtools/build"
)

// A Label represents a label of a build target in Bazel. Labels have three
// parts: a repository name, a package name, and a target name, formatted
// as @repo//pkg:target.
type Label struct {
	// Repo is the repository name. If omitted, the label refers to a target
	// in the current repository.
	Repo string

	// Pkg is the package name, which is usually the directory that contains
	// the target. If both Repo and Pkg are omitted, the label is relative.
	Pkg string

	// Name is the name of the target the label refers to. Name must not be empty.
	// Note that the name may be omitted from a label string if it is equal to
	// the last component of the package name ("//x" is equivalent to "//x:x"),
	// but in either case, Name should be set here.
	Name string

	// Relative indicates whether the label refers to a target in the current
	// package. Relative is true if and only if Repo and Pkg are both omitted.
	Relative bool

	// Canonical indicates whether the repository name is canonical. If true,
	// then the label will be stringified with an extra "@" prefix if it is
	// absolute.
	// Note: Label does not apply any kind of repo mapping.
	Canonical bool
}

// New constructs a new label from components.
func New(repo, pkg, name string) Label {
	return Label{Repo: repo, Pkg: pkg, Name: name}
}

// NoLabel is the zero value of Label. It is not a valid label and may be
// returned when an error occurs.
var NoLabel = Label{}

var (
	// This was taken from https://github.com/bazelbuild/bazel/blob/71fb1e4188b01e582a308cfe4bcbf1c730eded1b/src/main/java/com/google/devtools/build/lib/cmdline/RepositoryName.java#L159C1-L164
	// ~ and + are both allowed as the former is used in canonical repo names by Bazel 7 and earlier and the latter in Bazel 8.
	labelRepoRegexp = regexp.MustCompile(`^@$|^[A-Za-z0-9_.~+-]*$`)
	// This was taken from https://github.com/bazelbuild/bazel/blob/master/src/main/java/com/google/devtools/build/lib/cmdline/LabelValidator.java
	// Package names may contain all 7-bit ASCII characters except:
	// 0-31 (control characters)
	// 58 ':' (colon) - target name separator
	// 92 '\' (backslash) - directory separator (on Windows); may be allowed in the future
	// 127 (delete)
	// Target names may contain the same characters
	labelPkgRegexp  = regexp.MustCompile(`^[\x20-\x39\x3B-\x5B\x5D-\x7E]*$`)
	labelNameRegexp = labelPkgRegexp
)

// Parse reads a label from a string.
// See https://docs.bazel.build/versions/master/build-ref.html#lexi.
func Parse(s string) (Label, error) {
	origStr := s

	relative := true
	canonical := false
	var repo string
	if strings.HasPrefix(s, "@@") {
		s = s[len("@"):]
		canonical = true
	}
	if strings.HasPrefix(s, "@") {
		relative = false
		endRepo := strings.Index(s, "//")
		if endRepo > len("@") {
			repo = s[len("@"):endRepo]
			s = s[endRepo:]
			// If the label begins with "@//...", set repo = "@"
			// to remain distinct from "//...", where repo = ""
		} else if endRepo == len("@") {
			repo = s[:len("@")]
			s = s[len("@"):]
		} else {
			repo = s[len("@"):]
			s = "//:" + repo
		}
		if !labelRepoRegexp.MatchString(repo) {
			return NoLabel, fmt.Errorf("label parse error: repository has invalid characters: %q", origStr)
		}
	}

	var pkg string
	if strings.HasPrefix(s, "//") {
		relative = false
		endPkg := strings.Index(s, ":")
		if endPkg < 0 {
			pkg = s[len("//"):]
			s = ""
		} else {
			pkg = s[len("//"):endPkg]
			s = s[endPkg:]
		}
		if !labelPkgRegexp.MatchString(pkg) {
			return NoLabel, fmt.Errorf("label parse error: package has invalid characters: %q", origStr)
		}
	}

	if s == ":" {
		return NoLabel, fmt.Errorf("label parse error: empty name: %q", origStr)
	}
	name := strings.TrimPrefix(s, ":")
	if !labelNameRegexp.MatchString(name) {
		return NoLabel, fmt.Errorf("label parse error: name has invalid characters: %q", origStr)
	}

	if pkg == "" && name == "" {
		return NoLabel, fmt.Errorf("label parse error: empty package and name: %q", origStr)
	}
	if name == "" {
		name = path.Base(pkg)
	}

	return Label{
		Repo:      repo,
		Pkg:       pkg,
		Name:      name,
		Relative:  relative,
		Canonical: canonical,
	}, nil
}

func (l Label) String() string {
	if l.Relative {
		return fmt.Sprintf(":%s", l.Name)
	}

	var repo string
	if l.Repo != "" && l.Repo != "@" {
		repo = fmt.Sprintf("@%s", l.Repo)
	} else {
		// if l.Repo == "", the label string will begin with "//"
		// if l.Repo == "@", the label string will begin with "@//"
		repo = l.Repo
	}
	if l.Canonical && strings.HasPrefix(repo, "@") {
		repo = "@" + repo
	}

	if path.Base(l.Pkg) == l.Name {
		return fmt.Sprintf("%s//%s", repo, l.Pkg)
	}
	return fmt.Sprintf("%s//%s:%s", repo, l.Pkg, l.Name)
}

// Abs computes an absolute label (one with a repository and package name)
// from this label. If this label is already absolute, it is returned
// unchanged.
func (l Label) Abs(repo, pkg string) Label {
	if !l.Relative {
		return l
	}
	return Label{Repo: repo, Pkg: pkg, Name: l.Name}
}

// Rel attempts to compute a relative label from this label. If this label
// is already relative or is in a different package, this label may be
// returned unchanged.
func (l Label) Rel(repo, pkg string) Label {
	if l.Relative || l.Repo != repo {
		return l
	}
	if l.Pkg == pkg {
		return Label{Name: l.Name, Relative: true}
	}
	return Label{Pkg: l.Pkg, Name: l.Name}
}

// Equal returns whether two labels are exactly the same. It does not return
// true for different labels that refer to the same target.
func (l Label) Equal(other Label) bool {
	return l.Repo == other.Repo &&
		l.Pkg == other.Pkg &&
		l.Name == other.Name &&
		l.Relative == other.Relative
}

// Contains returns whether other is contained by the package of l or a
// sub-package. Neither label may be relative.
func (l Label) Contains(other Label) bool {
	if l.Relative {
		log.Panicf("l must not be relative: %s", l)
	}
	if other.Relative {
		log.Panicf("other must not be relative: %s", other)
	}
	result := l.Repo == other.Repo && pathtools.HasPrefix(other.Pkg, l.Pkg)
	return result
}

func (l Label) BzlExpr() bzl.Expr {
	return &bzl.StringExpr{
		Value: l.String(),
	}
}

var nonWordRe = regexp.MustCompile(`\W+`)

// ImportPathToBazelRepoName converts a Go import path into a bazel repo name
// following the guidelines in http://bazel.io/docs/be/functions.html#workspace
func ImportPathToBazelRepoName(importpath string) string {
	importpath = strings.ToLower(importpath)
	components := strings.Split(importpath, "/")
	labels := strings.Split(components[0], ".")
	reversed := make([]string, 0, len(labels)+len(components)-1)
	for i := range labels {
		l := labels[len(labels)-i-1]
		reversed = append(reversed, l)
	}
	repo := strings.Join(append(reversed, components[1:]...), ".")
	return nonWordRe.ReplaceAllString(repo, "_")
}
