// 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 (
	"flag"
	"fmt"
	"log"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/bazelbuild/bazel-gazelle/config"
	"github.com/bazelbuild/bazel-gazelle/rule"
	"github.com/bmatcuk/doublestar/v4"

	"github.com/bazel-contrib/rules_python/gazelle/pythonconfig"
)

// Configurer satisfies the config.Configurer interface. It's the
// language-specific configuration extension.
type Configurer struct{}

// RegisterFlags registers command-line flags used by the extension. This
// method is called once with the root configuration when Gazelle
// starts. RegisterFlags may set an initial values in Config.Exts. When flags
// are set, they should modify these values.
func (py *Configurer) RegisterFlags(fs *flag.FlagSet, cmd string, c *config.Config) {}

// CheckFlags validates the configuration after command line flags are parsed.
// This is called once with the root configuration when Gazelle starts.
// CheckFlags may set default values in flags or make implied changes.
func (py *Configurer) CheckFlags(fs *flag.FlagSet, c *config.Config) error {
	return nil
}

// KnownDirectives returns a list of directive keys that this Configurer can
// interpret. Gazelle prints errors for directives that are not recoginized by
// any Configurer.
func (py *Configurer) KnownDirectives() []string {
	return []string{
		pythonconfig.PythonExtensionDirective,
		pythonconfig.PythonRootDirective,
		pythonconfig.PythonManifestFileNameDirective,
		pythonconfig.IgnoreFilesDirective,
		pythonconfig.IgnoreDependenciesDirective,
		pythonconfig.ValidateImportStatementsDirective,
		pythonconfig.GenerationMode,
		pythonconfig.GenerationModePerFileIncludeInit,
		pythonconfig.GenerationModePerPackageRequireTestEntryPoint,
		pythonconfig.LibraryNamingConvention,
		pythonconfig.BinaryNamingConvention,
		pythonconfig.TestNamingConvention,
		pythonconfig.ProtoNamingConvention,
		pythonconfig.DefaultVisibilty,
		pythonconfig.Visibility,
		pythonconfig.TestFilePattern,
		pythonconfig.LabelConvention,
		pythonconfig.LabelNormalization,
		pythonconfig.GeneratePyiDeps,
		pythonconfig.ExperimentalAllowRelativeImports,
		pythonconfig.GenerateProto,
		pythonconfig.PythonResolveSiblingImports,
	}
}

// Configure modifies the configuration using directives and other information
// extracted from a build file. Configure is called in each directory.
//
// c is the configuration for the current directory. It starts out as a copy
// of the configuration for the parent directory.
//
// rel is the slash-separated relative path from the repository root to
// the current directory. It is "" for the root directory itself.
//
// f is the build file for the current directory or nil if there is no
// existing build file.
func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) {
	// Create the root config.
	if _, exists := c.Exts[languageName]; !exists {
		rootConfig := pythonconfig.New(c.RepoRoot, "")
		c.Exts[languageName] = pythonconfig.Configs{"": rootConfig}
	}

	configs := c.Exts[languageName].(pythonconfig.Configs)

	config, exists := configs[rel]
	if !exists {
		parent := configs.ParentForPackage(rel)
		config = parent.NewChild()
		configs[rel] = config
	}

	if f == nil {
		return
	}

	gazelleManifestFilename := "gazelle_python.yaml"

	for _, d := range f.Directives {
		switch d.Key {
		case "exclude":
			// We record the exclude directive for coarse-grained packages
			// since we do manual tree traversal in this mode.
			config.AddExcludedPattern(filepath.Join(rel, strings.TrimSpace(d.Value)))
		case pythonconfig.PythonExtensionDirective:
			switch d.Value {
			case "enabled":
				config.SetExtensionEnabled(true)
			case "disabled":
				config.SetExtensionEnabled(false)
			default:
				err := fmt.Errorf("invalid value for directive %q: %s: possible values are enabled/disabled",
					pythonconfig.PythonExtensionDirective, d.Value)
				log.Fatal(err)
			}
		case pythonconfig.PythonRootDirective:
			config.SetPythonProjectRoot(rel)
			config.SetDefaultVisibility([]string{fmt.Sprintf(pythonconfig.DefaultVisibilityFmtString, rel)})
		case pythonconfig.PythonManifestFileNameDirective:
			gazelleManifestFilename = strings.TrimSpace(d.Value)
		case pythonconfig.IgnoreFilesDirective:
			for _, ignoreFile := range strings.Split(d.Value, ",") {
				config.AddIgnoreFile(ignoreFile)
			}
		case pythonconfig.IgnoreDependenciesDirective:
			for _, ignoreDependency := range strings.Split(d.Value, ",") {
				config.AddIgnoreDependency(ignoreDependency)
			}
		case pythonconfig.ValidateImportStatementsDirective:
			v, err := strconv.ParseBool(strings.TrimSpace(d.Value))
			if err != nil {
				log.Fatal(err)
			}
			config.SetValidateImportStatements(v)
		case pythonconfig.GenerationMode:
			switch pythonconfig.GenerationModeType(strings.TrimSpace(d.Value)) {
			case pythonconfig.GenerationModePackage:
				config.SetCoarseGrainedGeneration(false)
				config.SetPerFileGeneration(false)
			case pythonconfig.GenerationModeFile:
				config.SetCoarseGrainedGeneration(false)
				config.SetPerFileGeneration(true)
			case pythonconfig.GenerationModeProject:
				config.SetCoarseGrainedGeneration(true)
				config.SetPerFileGeneration(false)
			default:
				err := fmt.Errorf("invalid value for directive %q: %s",
					pythonconfig.GenerationMode, d.Value)
				log.Fatal(err)
			}
		case pythonconfig.GenerationModePerFileIncludeInit:
			v, err := strconv.ParseBool(strings.TrimSpace(d.Value))
			if err != nil {
				log.Fatal(err)
			}
			config.SetPerFileGenerationIncludeInit(v)
		case pythonconfig.GenerationModePerPackageRequireTestEntryPoint:
			v, err := strconv.ParseBool(strings.TrimSpace(d.Value))
			if err != nil {
				log.Printf("invalid value for gazelle:%s in %q: %q",
					pythonconfig.GenerationModePerPackageRequireTestEntryPoint, rel, d.Value)
			} else {
				config.SetPerPackageGenerationRequireTestEntryPoint(v)
			}
		case pythonconfig.LibraryNamingConvention:
			config.SetLibraryNamingConvention(strings.TrimSpace(d.Value))
		case pythonconfig.BinaryNamingConvention:
			config.SetBinaryNamingConvention(strings.TrimSpace(d.Value))
		case pythonconfig.TestNamingConvention:
			config.SetTestNamingConvention(strings.TrimSpace(d.Value))
		case pythonconfig.ProtoNamingConvention:
			config.SetProtoNamingConvention(strings.TrimSpace(d.Value))
		case pythonconfig.DefaultVisibilty:
			switch directiveArg := strings.TrimSpace(d.Value); directiveArg {
			case "NONE":
				config.SetDefaultVisibility([]string{})
			case "DEFAULT":
				pythonProjectRoot := config.PythonProjectRoot()
				defaultVisibility := fmt.Sprintf(pythonconfig.DefaultVisibilityFmtString, pythonProjectRoot)
				config.SetDefaultVisibility([]string{defaultVisibility})
			default:
				// Handle injecting the python root. Assume that the user used the
				// exact string "$python_root$".
				labels := strings.ReplaceAll(directiveArg, "$python_root$", config.PythonProjectRoot())
				config.SetDefaultVisibility(strings.Split(labels, ","))
			}
		case pythonconfig.Visibility:
			labels := strings.ReplaceAll(strings.TrimSpace(d.Value), "$python_root$", config.PythonProjectRoot())
			config.AppendVisibility(labels)
		case pythonconfig.TestFilePattern:
			value := strings.TrimSpace(d.Value)
			if value == "" {
				log.Fatal("directive 'python_test_file_pattern' requires a value")
			}
			globStrings := strings.Split(value, ",")
			for _, g := range globStrings {
				if !doublestar.ValidatePattern(g) {
					log.Fatalf("invalid glob pattern '%s'", g)
				}
			}
			config.SetTestFilePattern(globStrings)
		case pythonconfig.LabelConvention:
			value := strings.TrimSpace(d.Value)
			if value == "" {
				log.Fatalf("directive '%s' requires a value", pythonconfig.LabelConvention)
			}
			config.SetLabelConvention(value)
		case pythonconfig.LabelNormalization:
			switch directiveArg := strings.ToLower(strings.TrimSpace(d.Value)); directiveArg {
			case "pep503":
				config.SetLabelNormalization(pythonconfig.Pep503LabelNormalizationType)
			case "none":
				config.SetLabelNormalization(pythonconfig.NoLabelNormalizationType)
			case "snake_case":
				config.SetLabelNormalization(pythonconfig.SnakeCaseLabelNormalizationType)
			default:
				config.SetLabelNormalization(pythonconfig.DefaultLabelNormalizationType)
			}
		case pythonconfig.ExperimentalAllowRelativeImports:
			v, err := strconv.ParseBool(strings.TrimSpace(d.Value))
			if err != nil {
				log.Printf("invalid value for gazelle:%s in %q: %q",
					pythonconfig.ExperimentalAllowRelativeImports, rel, d.Value)
			}
			config.SetExperimentalAllowRelativeImports(v)
		case pythonconfig.GeneratePyiDeps:
			v, err := strconv.ParseBool(strings.TrimSpace(d.Value))
			if err != nil {
				log.Fatal(err)
			}
			config.SetGeneratePyiDeps(v)
		case pythonconfig.GenerateProto:
			v, err := strconv.ParseBool(strings.TrimSpace(d.Value))
			if err != nil {
				log.Fatal(err)
			}
			config.SetGenerateProto(v)
		case pythonconfig.PythonResolveSiblingImports:
			v, err := strconv.ParseBool(strings.TrimSpace(d.Value))
			if err != nil {
				log.Fatal(err)
			}
			config.SetResolveSiblingImports(v)
		}
	}

	gazelleManifestPath := filepath.Join(c.RepoRoot, rel, gazelleManifestFilename)
	config.SetGazelleManifestPath(gazelleManifestPath)
}
