// Copyright 2022 The Chromium 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 main

import (
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"regexp"
	"strings"
)

type depConfig struct {
	bazelNameOverride string // Bazel style uses underscores not dashes, so we fix those if needed.
	needsBazelFile    bool
	patches           []string
	patchArgs         []string
	patchCmds         []string
	patchCmdsWin      []string
	isIndirect        bool // if True, it's used by another dependency, not by Skia directly
}

// These are all C++ deps or Rust deps (with a compatible C++ FFI) used by the Bazel build.
// They are a subset of those listed in DEPS.
// The key is the name of the repo as specified in DEPS.
var depsOverrides = map[string]depConfig{
	"abseil-cpp": {
		bazelNameOverride: "abseil_cpp",
		isIndirect:        true,
		patchCmds: []string{
			"find . -name BUILD.bazel -type f -exec sed -i.bak -e '/do_not_use_for_gloop_visibility_only/d' {} +",
			"find . -name '*.bak' -type f -delete",
		},
		patchCmdsWin: []string{
			`Get-ChildItem -Recurse -Filter BUILD.bazel | ForEach-Object { $p = $_.FullName; (Get-Content $p) -notmatch 'do_not_use_for_gloop_visibility_only' | Set-Content -Force -Encoding Ascii $p }`,
		},
	},
	"brotli":      {isIndirect: true},
	"highway":     {isIndirect: true},
	"spirv-tools": {bazelNameOverride: "spirv_tools"},
	// This name is important because spirv_tools expects @spirv_headers to exist by that name.
	"spirv-headers": {bazelNameOverride: "spirv_headers"},

	"dawn":           {needsBazelFile: true},
	"delaunator-cpp": {bazelNameOverride: "delaunator", needsBazelFile: true},
	"dng_sdk":        {needsBazelFile: true},
	"egl-registry":   {bazelNameOverride: "egl_registry", needsBazelFile: true},
	"expat": {
		needsBazelFile: true,
		patches:        []string{"//bazel/external/expat:config_files.patch"},
	},
	"freetype": {
		needsBazelFile: true,
		patches:        []string{"//bazel/external/freetype:config_files.patch"},
	},
	"harfbuzz": {
		needsBazelFile: true,
		patches:        []string{"//bazel/external/harfbuzz:config_files.patch"},
	},
	"icu": {
		needsBazelFile: true,
		patches:        []string{"//bazel/external/icu:icu_utils.patch"},
		patchCmds: []string{
			"rm source/i18n/BUILD.bazel",
			"rm source/common/BUILD.bazel",
			"rm source/stubdata/BUILD.bazel",
		},
		patchCmdsWin: []string{
			"del source/i18n/BUILD.bazel",
			"del source/common/BUILD.bazel",
			"del source/stubdata/BUILD.bazel",
		},
	},
	"icu4x": {needsBazelFile: true},
	"imgui": {needsBazelFile: true},
	"jinja2": {
		needsBazelFile: true,
		patchCmds: []string{
			"mkdir tmp_jinja2",
			"mv * tmp_jinja2 || true",
			"mv tmp_jinja2 jinja2",
			"mv jinja2/BUILD.bazel .",
		},
	},
	"libavif":       {needsBazelFile: true},
	"libgav1":       {needsBazelFile: true},
	"libjpeg-turbo": {bazelNameOverride: "libjpeg_turbo", needsBazelFile: true},
	"libjxl": {
		needsBazelFile: true,
		patchCmds: []string{
			"rm -f lib/BUILD tools/BUILD",
		},
		patchCmdsWin: []string{
			"del /f /q lib\\BUILD tools\\BUILD",
		},
	},
	"libpng":  {needsBazelFile: true},
	"libwebp": {needsBazelFile: true},
	"libyuv":  {needsBazelFile: true},
	"markupsafe": {
		needsBazelFile: true,
		patchCmds: []string{
			"mkdir tmp_markupsafe",
			"mv * tmp_markupsafe || true",
			"mv tmp_markupsafe markupsafe",
			"mv markupsafe/BUILD.bazel .",
		},
	},
	"opengl-registry":          {bazelNameOverride: "opengl_registry", needsBazelFile: true},
	"spirv-cross":              {bazelNameOverride: "spirv_cross", needsBazelFile: true},
	"perfetto":                 {needsBazelFile: true},
	"piex":                     {needsBazelFile: true},
	"vello":                    {needsBazelFile: true},
	"vulkan-headers":           {bazelNameOverride: "vulkan_headers", needsBazelFile: true},
	"vulkan-tools":             {bazelNameOverride: "vulkan_tools", needsBazelFile: true},
	"vulkan-utility-libraries": {bazelNameOverride: "vulkan_utility_libraries", needsBazelFile: true},
	"vulkanmemoryallocator":    {needsBazelFile: true},
	"d3d12allocator":           {needsBazelFile: true},
	"wuffs":                    {needsBazelFile: true},
	"zlib":                     {needsBazelFile: true},
}

func main() {
	var (
		depsFile    = flag.String("deps_file", "DEPS", "The location of the DEPS file. Usually at the root of the repository")
		genJSONFile = flag.String("gen_json_file", "bazel/deps.json", "The location of the .json file that has the generated data used to make Bazel git_repository rules.")
		// https://bazel.build/docs/user-manual#running-executables
		repoDir = flag.String("repo_dir", os.Getenv("BUILD_WORKSPACE_DIRECTORY"), "The root directory of the repo. Default set by BUILD_WORKSPACE_DIRECTORY env variable.")
	)
	flag.Parse()

	if *repoDir == "" {
		fmt.Println(`Must set --repo_dir
This is done automatically via:
    bazel run //bazel/deps_parser`)
		os.Exit(1)
	}

	fmt.Println(os.Environ())

	if *depsFile == "" || *genJSONFile == "" {
		fmt.Println("Must set --deps_file and --gen_json_file")
		flag.PrintDefaults()
	}

	if err := os.Chdir(*repoDir); err != nil {
		fmt.Printf("Could not cd to %s\n", *repoDir)
		os.Exit(1)
	}

	b, err := os.ReadFile(*depsFile)
	if err != nil {
		fmt.Printf("Could not open %s: %s\n", *depsFile, err)
		os.Exit(1)
	}
	contents := strings.Split(string(b), "\n")

	outputFile, count, err := parseDEPSFile(contents)
	if err != nil {
		fmt.Printf("Parsing error %s\n", err)
		os.Exit(1)
	}
	if err := moveWithCopyBackup(outputFile, *genJSONFile); err != nil {
		fmt.Printf("Could not write to generated .bzl file: %s\n", err)
		os.Exit(1)
	}
	fmt.Printf("Wrote %d deps\n", count)
}

type repoConfig struct {
	// https://bazel.build/rules/lib/repo/git
	BuildFile    string   `json:"build_file,omitempty"`
	Commit       string   `json:"commit"`
	Name         string   `json:"name"`
	PatchArgs    []string `json:"patch_args,omitempty"`
	PatchCmds    []string `json:"patch_cmds,omitempty"`
	PatchCmdsWin []string `json:"patch_cmds_win,omitempty"`
	Patches      []string `json:"patches,omitempty"`
	Remote       string   `json:"remote"`
}

type deps struct {
	Warning  string       `json:"!note"`
	Direct   []repoConfig `json:"direct"`
	Indirect []repoConfig `json:"indirect"`
}

func parseDEPSFile(contents []string) (string, int, error) {
	depsLine := regexp.MustCompile(`externals/(\S+)".+"(https.+)@([a-f0-9]+)"`)
	outputFile, err := os.CreateTemp("", "genbzl")
	if err != nil {
		return "", 0, fmt.Errorf("Could not create output file: %s\n", err)
	}
	defer outputFile.Close()

	var d deps
	d.Warning = "DO NOT MODIFY BY HAND. Instead, bazelisk run //bazel/deps_parser"
	for _, line := range contents {
		if match := depsLine.FindStringSubmatch(line); len(match) > 0 {
			rc := repoConfig{Name: match[1], Remote: match[2], Commit: match[3]}

			cfg, ok := depsOverrides[rc.Name]
			if !ok {
				continue
			}
			if cfg.bazelNameOverride != "" {
				rc.Name = cfg.bazelNameOverride
			}
			rc.Patches = cfg.patches
			rc.PatchArgs = cfg.patchArgs
			rc.PatchCmds = cfg.patchCmds
			rc.PatchCmdsWin = cfg.patchCmdsWin
			if cfg.needsBazelFile {
				rc.BuildFile = fmt.Sprintf("//bazel/external/%s:BUILD.bazel", rc.Name)
			}
			if cfg.isIndirect {
				d.Indirect = append(d.Indirect, rc)
			} else {
				d.Direct = append(d.Direct, rc)
			}

		}
	}

	je := json.NewEncoder(outputFile)
	je.SetIndent("", "  ")
	if err := je.Encode(d); err != nil {
		return "", 0, fmt.Errorf("Could not encode json file: %s\n", err)
	}

	return outputFile.Name(), len(d.Direct) + len(d.Indirect), nil
}

func moveWithCopyBackup(src, dst string) error {
	// Atomically rename temp file to workspace. This should minimize the chance of corruption
	// or writing a partial file if there is an error or the program is interrupted.
	if err := os.Rename(src, dst); err != nil {
		// Errors can happen if the temporary file is on a different partition than the Skia
		// codebase. In that case, do a manual read/write to copy the data. See
		// https://github.com/jenkins-x/jx/issues/449 for a similar issue
		if strings.Contains(err.Error(), "invalid cross-device link") {
			bytes, err := os.ReadFile(src)
			if err != nil {
				return fmt.Errorf("Could not do backup read from %s: %s\n", src, err)
			}
			if err := os.WriteFile(dst, bytes, 0644); err != nil {
				return fmt.Errorf("Could not do backup write of %d bytes to %s: %s\n", len(bytes), dst, err)
			}
			// Backup "move" successful
			return nil
		}
		return fmt.Errorf("Could not write %s -> %s: %s\n", src, dst, err)
	}
	return nil
}
