// Copyright 2024 The BoringSSL Authors
//
// 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
//
//     https://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 main

import (
	"bytes"
	"cmp"
	"encoding/json"
	"fmt"
	"path"
	"path/filepath"
	"slices"
	"strings"
	"sync"

	"boringssl.googlesource.com/boringssl.git/util/build"
)

// An InputTarget is a build target with build inputs that still need to be
// pregenerated. All file lists in InputTarget are interpreted with glob
// patterns as in filepath.Glob.
type InputTarget struct {
	build.Target
	// ErrData contains a list of errordata files to combine into err_data.cc.
	ErrData []string `json:"err_data,omitempty"`
	// The following fields define perlasm sources for the corresponding
	// architecture.
	PerlasmAarch64 []PerlasmSource `json:"perlasm_aarch64,omitempty"`
	PerlasmArm     []PerlasmSource `json:"perlasm_arm,omitempty"`
	PerlasmX86     []PerlasmSource `json:"perlasm_x86,omitempty"`
	PerlasmX86_64  []PerlasmSource `json:"perlasm_x86_64,omitempty"`
	// Hdrs are C++ headers.
	Hdrs []string `json:"hdrs,omitempty"`
}

type PerlasmSource struct {
	// Src the path to the input perlasm file.
	Src string `json:"src"`
	// Dst, if not empty, is base name of the destination file. If empty, this
	// is determined from Src by default. It should be overridden if a single
	// source file generates multiple functions (e.g. SHA-256 vs SHA-512) or
	// multiple architectures (e.g. the "armx" files).
	Dst string `json:"dst,omitempty"`
	// Args is a list of extra parameters to pass to the script.
	Args []string `json:"args,omitempty"`
}

// Pregenerate converts an input target to an output target. It returns the
// result alongside a list of tasks that must be run to build the referenced
// files.
func (in *InputTarget) Pregenerate(name string) (out build.Target, tasks []*Task, asmSrcs []string, err error) {
	// Expand wildcards.
	out.Srcs, err = glob(in.Srcs)
	if err != nil {
		return
	}
	out.Hdrs, err = glob(in.Hdrs)
	if err != nil {
		return
	}
	out.InternalHdrs, err = glob(in.InternalHdrs)
	if err != nil {
		return
	}
	out.Asm, err = glob(in.Asm)
	if err != nil {
		return
	}
	out.Nasm, err = glob(in.Nasm)
	if err != nil {
		return
	}
	out.Data, err = glob(in.Data)
	if err != nil {
		return
	}

	asmSrcs = append(asmSrcs, out.Asm...)
	asmSrcs = append(asmSrcs, out.Nasm...)

	addTask := func(list *[]string, t *Task) {
		tasks = append(tasks, t)
		*list = append(*list, t.Destination)
	}

	if len(in.ErrData) != 0 {
		var inputs []string
		inputs, err = glob(in.ErrData)
		if err != nil {
			return
		}
		addTask(&out.Srcs, NewErrDataTask(name, inputs))
	}

	addPerlasmTask := func(list *[]string, p *PerlasmSource, fileSuffix string, args []string) string {
		dst := p.Dst
		if len(p.Dst) == 0 {
			dst = strings.TrimSuffix(path.Base(p.Src), ".pl")
		}
		dst = path.Join("gen", name, dst+fileSuffix)
		args = append(slices.Clone(args), p.Args...)
		addTask(list, NewPerlasmTask(dst, p.Src, args))
		return dst
	}

	for _, p := range in.PerlasmAarch64 {
		asmSrcs = append(asmSrcs,
			addPerlasmTask(&out.Asm, &p, "-apple.S", []string{"ios64"}),
			addPerlasmTask(&out.Asm, &p, "-linux.S", []string{"linux64"}),
			addPerlasmTask(&out.Asm, &p, "-win.S", []string{"win64"}))
	}
	for _, p := range in.PerlasmArm {
		asmSrcs = append(asmSrcs, addPerlasmTask(&out.Asm, &p, "-linux.S", []string{"linux32"}))
	}
	for _, p := range in.PerlasmX86 {
		asmSrcs = append(asmSrcs,
			addPerlasmTask(&out.Asm, &p, "-apple.S", []string{"macosx", "-fPIC"}),
			addPerlasmTask(&out.Asm, &p, "-linux.S", []string{"elf", "-fPIC"}),
			addPerlasmTask(&out.Nasm, &p, "-win.asm", []string{"win32n", "-fPIC"}))
	}
	for _, p := range in.PerlasmX86_64 {
		asmSrcs = append(asmSrcs,
			addPerlasmTask(&out.Asm, &p, "-apple.S", []string{"macosx"}),
			addPerlasmTask(&out.Asm, &p, "-linux.S", []string{"elf"}),
			addPerlasmTask(&out.Nasm, &p, "-win.asm", []string{"nasm"}))
	}

	// Re-sort the modified fields.
	slices.Sort(out.Srcs)
	slices.Sort(out.Asm)
	slices.Sort(out.Nasm)

	return
}

func glob(paths []string) ([]string, error) {
	var ret []string
	for _, path := range paths {
		if !strings.ContainsRune(path, '*') {
			ret = append(ret, path)
			continue
		}
		matches, err := filepath.Glob(path)
		if err != nil {
			return nil, err
		}
		if len(matches) == 0 {
			return nil, fmt.Errorf("glob matched no files: %q", path)
		}
		// Switch from Windows to POSIX paths.
		for _, match := range matches {
			ret = append(ret, strings.ReplaceAll(match, "\\", "/"))
		}
	}
	slices.Sort(ret)
	return ret, nil
}

func sortedKeys[K cmp.Ordered, V any](m map[K]V) []K {
	keys := make([]K, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	slices.Sort(keys)
	return keys
}

func writeHeader(b *bytes.Buffer, comment string) {
	fmt.Fprintf(b, "%s Copyright 2024 The BoringSSL Authors\n", comment)
	fmt.Fprintf(b, "%s\n", comment)
	fmt.Fprintf(b, "%s Licensed under the Apache License, Version 2.0 (the \"License\");\n", comment)
	fmt.Fprintf(b, "%s you may not use this file except in compliance with the License.\n", comment)
	fmt.Fprintf(b, "%s You may obtain a copy of the License at\n", comment)
	fmt.Fprintf(b, "%s\n", comment)
	fmt.Fprintf(b, "%s     https://www.apache.org/licenses/LICENSE-2.0\n", comment)
	fmt.Fprintf(b, "%s\n", comment)
	fmt.Fprintf(b, "%s Unless required by applicable law or agreed to in writing, software\n", comment)
	fmt.Fprintf(b, "%s distributed under the License is distributed on an \"AS IS\" BASIS,\n", comment)
	fmt.Fprintf(b, "%s WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", comment)
	fmt.Fprintf(b, "%s See the License for the specific language governing permissions and\n", comment)
	fmt.Fprintf(b, "%s limitations under the License.\n", comment)
	fmt.Fprintf(b, "\n")
	fmt.Fprintf(b, "%s Generated by go ./util/pregenerate. Do not edit manually.\n", comment)
}

func buildVariablesTask(targets map[string]build.Target, dst, comment string, writeVariable func(b *bytes.Buffer, name string, val []string)) *Task {
	return NewSimpleTask("sources", dst, func() ([]byte, error) {
		var b bytes.Buffer
		writeHeader(&b, comment)

		for _, name := range sortedKeys(targets) {
			target := targets[name]
			if len(target.Srcs) != 0 {
				writeVariable(&b, name+"_sources", target.Srcs)
			}
			if len(target.Hdrs) != 0 {
				writeVariable(&b, name+"_headers", target.Hdrs)
			}
			if len(target.InternalHdrs) != 0 {
				writeVariable(&b, name+"_internal_headers", target.InternalHdrs)
			}
			if len(target.Asm) != 0 {
				writeVariable(&b, name+"_sources_asm", target.Asm)
			}
			if len(target.Nasm) != 0 {
				writeVariable(&b, name+"_sources_nasm", target.Nasm)
			}
			if len(target.Data) != 0 {
				writeVariable(&b, name+"_data", target.Data)
			}
		}

		return b.Bytes(), nil
	})
}

func writeBazelVariable(b *bytes.Buffer, name string, val []string) {
	fmt.Fprintf(b, "\n%s = [\n", name)
	for _, v := range val {
		fmt.Fprintf(b, "    %q,\n", v)
	}
	fmt.Fprintf(b, "]\n")
}

func writeCMakeVariable(b *bytes.Buffer, name string, val []string) {
	fmt.Fprintf(b, "\nset(\n")
	fmt.Fprintf(b, "  %s\n\n", strings.ToUpper(name))
	for _, v := range val {
		fmt.Fprintf(b, "  %s\n", v)
	}
	fmt.Fprintf(b, ")\n")
}

func writeMakeVariable(b *bytes.Buffer, name string, val []string) {
	// Prefix the variable names to avoid collisions. make builds often use
	// by inclusion, so variables may not be scoped.
	fmt.Fprintf(b, "\nboringssl_%s := \\\n", name)
	for i, v := range val {
		if i == len(val)-1 {
			fmt.Fprintf(b, "  %s\n", v)
		} else {
			fmt.Fprintf(b, "  %s \\\n", v)
		}
	}
}

func writeGNVariable(b *bytes.Buffer, name string, val []string) {
	fmt.Fprintf(b, "\n%s = [\n", name)
	for _, v := range val {
		fmt.Fprintf(b, "  %q,\n", v)
	}
	fmt.Fprintf(b, "]\n")
}

func jsonTask(targets map[string]build.Target, dst string) *Task {
	return NewSimpleTask("sources", dst, func() ([]byte, error) {
		return json.MarshalIndent(targets, "", "  ")
	})
}

func soongTask(targets map[string]build.Target, dst string) *Task {
	return NewSimpleTask("sources", dst, func() ([]byte, error) {
		var b bytes.Buffer
		writeHeader(&b, "//")

		writeAttribute := func(indent, name string, val []string) {
			fmt.Fprintf(&b, "%s%s: [\n", indent, name)
			for _, v := range val {
				fmt.Fprintf(&b, "%s    %q,\n", indent, v)
			}
			fmt.Fprintf(&b, "%s],\n", indent)

		}

		for _, name := range sortedKeys(targets) {
			target := targets[name]
			fmt.Fprintf(&b, "\ncc_defaults {\n")
			fmt.Fprintf(&b, "    name: %q\n", "boringssl_"+name+"_sources")
			if len(target.Srcs) != 0 {
				writeAttribute("    ", "srcs", target.Srcs)
			}
			if len(target.Data) != 0 {
				writeAttribute("    ", "data", target.Data)
			}
			if len(target.Asm) != 0 {
				fmt.Fprintf(&b, "    target: {\n")
				// Only emit asm for Linux. On Windows, BoringSSL requires NASM, which is
				// not available in AOSP. On Darwin, the assembly works fine, but it
				// conflicts with Android's FIPS build. See b/294399371.
				fmt.Fprintf(&b, "        linux: {\n")
				writeAttribute("            ", "srcs", target.Asm)
				fmt.Fprintf(&b, "        },\n")
				fmt.Fprintf(&b, "        darwin: {\n")
				fmt.Fprintf(&b, "            cflags: [\"-DOPENSSL_NO_ASM\"],\n")
				fmt.Fprintf(&b, "        },\n")
				fmt.Fprintf(&b, "        windows: {\n")
				fmt.Fprintf(&b, "            cflags: [\"-DOPENSSL_NO_ASM\"],\n")
				fmt.Fprintf(&b, "        },\n")
				fmt.Fprintf(&b, "    },\n")
			}
			fmt.Fprintf(&b, "},\n")
		}

		return b.Bytes(), nil
	})
}

func MakeBuildFiles(targets map[string]build.Target) []*Task {
	// TODO(crbug.com/boringssl/542): Generate the build files for the other
	// types as well.
	return []*Task{
		buildVariablesTask(targets, "gen/sources.bzl", "#", writeBazelVariable),
		buildVariablesTask(targets, "gen/sources.cmake", "#", writeCMakeVariable),
		buildVariablesTask(targets, "gen/sources.gni", "#", writeGNVariable),
		buildVariablesTask(targets, "gen/sources.mk", "#", writeMakeVariable),
		jsonTask(targets, "gen/sources.json"),
	}
}

// addGeneratedPublicHeader adds a generated public `header` to `targetsOut`.
func addGeneratedPublicHeader(targetsOut map[string]build.Target, header string) {
	target := targetsOut["crypto"]
	target.Hdrs = append(target.Hdrs, header)
	slices.Sort(target.Hdrs)
	targetsOut["crypto"] = target
}

// addGeneratedInternalHeader adds a generated internal `header` to `targetsOut`.
func addGeneratedInternalHeader(targetsOut map[string]build.Target, header string) {
	target := targetsOut["crypto"]
	target.InternalHdrs = append(target.InternalHdrs, header)
	slices.Sort(target.InternalHdrs)
	targetsOut["crypto"] = target
}

// Construct a task to collect assembly global symbols into the file "gen/asm.syms".
// This task should only run after all the `PerlAsmTask`s in `perlAsmTasks` complete.
func MakeCollectAsmGlobalTasks(perlAsmTasks []*Task, allAsmSrcs []string, targetsOut map[string]build.Target) []*Task {
	var syms []string
	var err error
	buildHeadersOnce := func() {
		syms, err = CollectAsmGlobals(allAsmSrcs)
	}
	var once sync.Once
	addGeneratedInternalHeader(targetsOut, "include/openssl/prefix_symbols_internal_c.h")
	addGeneratedInternalHeader(targetsOut, "include/openssl/prefix_symbols_internal_S.h")
	addGeneratedInternalHeader(targetsOut, "gen/boringssl_prefix_symbols_internal_x86_win_asm.inc")
	addGeneratedInternalHeader(targetsOut, "gen/boringssl_prefix_symbols_internal_x86_64_win_asm.inc")
	return []*Task{
		NewSimpleTask("prefix_symbols_internal", "include/openssl/prefix_symbols_internal_c.h", func() ([]byte, error) {
			once.Do(buildHeadersOnce)
			return BuildAsmGlobalsCHeader(syms), err
		}, perlAsmTasks...),
		NewSimpleTask("prefix_symbols_internal", "include/openssl/prefix_symbols_internal_S.h", func() ([]byte, error) {
			once.Do(buildHeadersOnce)
			return BuildAsmGlobalsGasHeader(syms), err
		}, perlAsmTasks...),
		NewSimpleTask("prefix_symbols_internal", "gen/boringssl_prefix_symbols_internal_x86_win_asm.inc", func() ([]byte, error) {
			once.Do(buildHeadersOnce)
			return BuildAsmGlobalsNasmX86Header(syms), err
		}, perlAsmTasks...),
		NewSimpleTask("prefix_symbols_internal", "gen/boringssl_prefix_symbols_internal_x86_64_win_asm.inc", func() ([]byte, error) {
			once.Do(buildHeadersOnce)
			return BuildAsmGlobalsNasmX8664Header(syms), err
		}, perlAsmTasks...),
	}
}

// MakePrefixingIncludes returns the tasks to generate the header files for symbol prefixing.
func MakePrefixingIncludes(in map[string]InputTarget, targetsOut map[string]build.Target) []*Task {
	addGeneratedPublicHeader(targetsOut, "include/openssl/prefix_symbols.h")
	return []*Task{
		NewSimpleTask("prefix_symbols", "include/openssl/prefix_symbols.h", func() ([]byte, error) {
			var headers []string
			for _, t := range in {
				if t.PrefixSymbols {
					headers = append(headers, t.Hdrs...)
				}
			}
			syms, err := CollectCSymbols(headers)
			return BuildCRenamingInclude(syms), err
		}),
	}
}
