// Copyright 2021 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//  1. Redistributions of source code must retain the above copyright notice, this
//     list of conditions and the following disclaimer.
//
//  2. Redistributions in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//
//  3. Neither the name of the copyright holder nor the names of its
//     contributors may be used to endorse or promote products derived from
//     this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

package templates

import (
	"context"
	"flag"
	"fmt"
	"math/rand"
	"path/filepath"
	"reflect"
	"strings"

	"dawn.googlesource.com/dawn/tools/src/cmd/gen/common"
	"dawn.googlesource.com/dawn/tools/src/container"
	"dawn.googlesource.com/dawn/tools/src/fileutils"
	"dawn.googlesource.com/dawn/tools/src/glob"
	"dawn.googlesource.com/dawn/tools/src/oswrapper"
	"dawn.googlesource.com/dawn/tools/src/template"
	"dawn.googlesource.com/dawn/tools/src/tint/intrinsic/gen"
	"dawn.googlesource.com/dawn/tools/src/tint/intrinsic/parser"
	"dawn.googlesource.com/dawn/tools/src/tint/intrinsic/resolver"
	"dawn.googlesource.com/dawn/tools/src/tint/intrinsic/sem"
)

func init() {
	common.Register(&CmdSources{})
	common.Register(&CmdTests{})
}

// CmdSources implements the "sources" command.
type CmdSources struct{}

func (c *CmdSources) Name() string { return "sources" }

func (c *CmdSources) Desc() string {
	return `sources generates files from <file>.tmpl files found in the Tint source directory`
}

func (c *CmdSources) RegisterFlags(ctx context.Context, cfg *common.Config) ([]string, error) {
	return nil, nil
}

func (c *CmdSources) Run(ctx context.Context, cfg *common.Config) error {
	args := flag.Args()
	outputDir := ""
	if len(args) == 1 {
		outputDir = args[0]
	} else if len(args) > 1 {
		return fmt.Errorf("too many arguments")
	}
	return runTemplates(ctx, cfg, "src/tint", outputDir)
}

// CmdTests implements the "tests" command.
type CmdTests struct{}

func (c *CmdTests) Name() string { return "tests" }

func (c *CmdTests) Desc() string {
	return `tests generates files from <file>.tmpl files found in the Tint test directory`
}

func (c *CmdTests) RegisterFlags(ctx context.Context, cfg *common.Config) ([]string, error) {
	return nil, nil
}

func (c *CmdTests) Run(ctx context.Context, cfg *common.Config) error {
	if len(flag.Args()) > 0 {
		return fmt.Errorf("arguments are not supported")
	}
	return runTemplates(ctx, cfg, "test/tint", "")
}

// TODO(crbug.com/344014313): Add unittest coverage.
// runTemplates executes the templates matching the defaultIncludes patterns.
func runTemplates(ctx context.Context, cfg *common.Config, dirPrefix string, outputDir string) error {
	staleFiles := common.StaleFiles{}
	projectRoot := filepath.Join(fileutils.DawnRoot(cfg.OsWrapper), dirPrefix)

	configStr := `{
		"paths": [{"include": ["**.tmpl"]}]
	}`
	files, err := glob.Scan(projectRoot, glob.MustParseConfig(configStr), cfg.OsWrapper)
	if err != nil {
		return err
	}

	cache := &genCache{
		fsReader: cfg.OsWrapper,
	}

	// For each template file...
	for _, relTmplPath := range files { // relative to project root
		if cfg.Flags.Verbose {
			fmt.Println("processing", relTmplPath)
		}
		// Make tmplPath absolute
		tmplPath := filepath.Join(projectRoot, relTmplPath)
		tmplDir := filepath.Dir(tmplPath)

		// Create or update the file at relPath if the file content has changed,
		// preserving the copyright year in the header.
		// relPath is a path relative to the template
		writeFile := func(relPath, body, commentPrefix string) error {
			if strings.TrimSpace(body) == "" {
				// Don't write empty files
				return nil
			}

			var outPath string
			if outputDir != "" {
				outPath = filepath.Join(outputDir, dirPrefix, filepath.Dir(relTmplPath), relPath)
			} else {
				outPath = filepath.Join(tmplDir, relPath)
			}

			// Load the old file
			existing, err := cfg.OsWrapper.ReadFile(outPath)
			if err != nil {
				existing = nil
			}

			// Write the common file header
			if cfg.Flags.Verbose {
				fmt.Println("  writing", outPath)
			}
			sb := strings.Builder{}
			sb.WriteString(common.Header(string(existing), filepath.ToSlash(filepath.Join(dirPrefix, relTmplPath)), commentPrefix))
			sb.WriteString("\n")
			sb.WriteString(body)
			oldContent, newContent := string(existing), sb.String()

			if oldContent != newContent {
				if cfg.Flags.CheckStale {
					staleFiles = append(staleFiles, outPath)
				} else {
					if err := cfg.OsWrapper.MkdirAll(filepath.Dir(outPath), 0777); err != nil {
						return fmt.Errorf("failed to create directory for '%v': %w", outPath, err)
					}
					if err := cfg.OsWrapper.WriteFile(outPath, []byte(newContent), 0666); err != nil {
						return fmt.Errorf("failed to write file '%v': %w", outPath, err)
					}
				}
			}

			return nil
		}

		// Write the content generated using the template and semantic info
		_, tmplFileName := filepath.Split(tmplPath)
		outPath := strings.TrimSuffix(tmplFileName, ".tmpl")
		if err := generate(tmplPath, outPath, cache, writeFile); err != nil {
			return fmt.Errorf("while processing '%v': %w", tmplPath, err)
		}
	}

	if len(staleFiles) > 0 {
		return staleFiles
	}

	return nil
}

type intrinsicCache struct {
	path           string
	cachedSem      *sem.Sem                   // lazily built by sem()
	cachedTable    *gen.IntrinsicTable        // lazily built by intrinsicTable()
	cachedPermuter *gen.Permutator            // lazily built by permute()
	fsReader       oswrapper.FilesystemReader // Stored reference to a FilesystemReader.
}

// Sem lazily parses and resolves the intrinsic.def file, returning the semantic info.
func (i *intrinsicCache) Sem() (*sem.Sem, error) {
	if i.cachedSem == nil {
		// Load the intrinsic definition file
		defPath := filepath.Join(fileutils.DawnRoot(i.fsReader), i.path)

		defSource, err := i.fsReader.ReadFile(defPath)
		if err != nil {
			return nil, err
		}

		// Parse the definition file to produce an AST
		ast, err := parser.Parse(string(defSource), i.path, i.fsReader)
		if err != nil {
			return nil, err
		}

		// Resolve the AST to produce the semantic info
		sem, err := resolver.Resolve(ast)
		if err != nil {
			return nil, err
		}

		i.cachedSem = sem
	}
	return i.cachedSem, nil
}

// Table lazily calls and returns the result of BuildIntrinsicTable(),
// caching the result for repeated calls.
func (i *intrinsicCache) Table() (*gen.IntrinsicTable, error) {
	if i.cachedTable == nil {
		sem, err := i.Sem()
		if err != nil {
			return nil, err
		}
		i.cachedTable, err = gen.BuildIntrinsicTable(sem)
		if err != nil {
			return nil, err
		}
	}
	return i.cachedTable, nil
}

// Permute lazily calls NewPermuter(), caching the result for repeated calls,
// then passes the argument to Permutator.Permute()
func (i *intrinsicCache) Permute(overload *sem.Overload) ([]gen.Permutation, error) {
	if i.cachedPermuter == nil {
		sem, err := i.Sem()
		if err != nil {
			return nil, err
		}
		i.cachedPermuter, err = gen.NewPermutator(sem)
		if err != nil {
			return nil, err
		}
	}
	out, err := i.cachedPermuter.Permute(overload)
	if err != nil {
		return nil, fmt.Errorf("while permuting '%v'\n%w", overload, err)
	}
	return out, nil
}

// Cache for objects that are expensive to build, and can be reused between templates.
type genCache struct {
	intrinsicsCache container.Map[string, *intrinsicCache]
	fsReader        oswrapper.FilesystemReader
}

func (g *genCache) intrinsics(path string) *intrinsicCache {
	if g.intrinsicsCache == nil {
		g.intrinsicsCache = container.NewMap[string, *intrinsicCache]()
	}
	i := g.intrinsicsCache[path]
	if i == nil {
		i = &intrinsicCache{path: path, fsReader: g.fsReader}
		g.intrinsicsCache[path] = i
	}
	return i
}

type generator struct {
	cache         *genCache
	writeFile     WriteFile
	rnd           *rand.Rand
	commentPrefix string
}

// setCommentPrefix sets the prefix used for comments, as used by the template
func (g *generator) setCommentPrefix(commentPrefix string) string {
	g.commentPrefix = commentPrefix
	return ""
}

// WriteFile is a function that Generate() may call to emit a new file from a
// template.
// relPath is the relative path from the currently executing template.
// content is the file content to write.
// comment is the prefix used for line comments
type WriteFile func(relPath, content, comment string) error

// generate executes the template tmpl, calling writeFile with the output.
// See https://golang.org/pkg/text/template/ for documentation on the template
// syntax.
func generate(tmplPath, outPath string, cache *genCache, writeFile WriteFile) error {
	g := generator{
		cache:         cache,
		writeFile:     writeFile,
		rnd:           rand.New(rand.NewSource(4561123)),
		commentPrefix: "//",
	}

	funcs := map[string]any{
		"SetCommentPrefix":                    g.setCommentPrefix,
		"SplitDisplayName":                    gen.SplitDisplayName,
		"Scramble":                            g.scramble,
		"IsEnumEntry":                         is(sem.EnumEntry{}),
		"IsEnumMatcher":                       is(sem.EnumMatcher{}),
		"IsFQN":                               is(sem.FullyQualifiedName{}),
		"IsInt":                               is(1),
		"IsTemplateEnumParam":                 is(sem.TemplateEnumParam{}),
		"IsTemplateNumberParam":               is(sem.TemplateNumberParam{}),
		"IsTemplateTypeParam":                 is(sem.TemplateTypeParam{}),
		"IsType":                              is(sem.Type{}),
		"ElementType":                         gen.ElementType,
		"DeepestElementType":                  gen.DeepestElementType,
		"IsAbstract":                          gen.IsAbstract,
		"IsDeclarable":                        gen.IsDeclarable,
		"IsHostShareable":                     gen.IsHostShareable,
		"OverloadUsesType":                    gen.OverloadUsesType,
		"OverloadUsesReadWriteStorageTexture": gen.OverloadUsesReadWriteStorageTexture,
		"OverloadNeedsDesktopGLSL":            gen.OverloadNeedsDesktopGLSL,
		"IsFirstIn":                           isFirstIn,
		"IsLastIn":                            isLastIn,
		"LoadIntrinsics":                      func(path string) *intrinsicCache { return g.cache.intrinsics(path) },
		"WriteFile": func(relPath, content string) (string, error) {
			return "", g.writeFile(relPath, content, g.commentPrefix)
		},
	}
	t, err := template.FromFile(tmplPath, cache.fsReader)
	if err != nil {
		return err
	}
	w := &strings.Builder{}
	if err := t.Run(w, nil, funcs); err != nil {
		return err
	}
	return writeFile(outPath, w.String(), g.commentPrefix)
}

// scramble randomly modifies the input string so that it is no longer equal to
// any of the strings in 'avoid'.
func (g *generator) scramble(str string, avoid container.Set[string]) (string, error) {
	bytes := []byte(str)
	passes := g.rnd.Intn(5) + 1

	const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"

	char := func() byte { return chars[g.rnd.Intn(len(chars))] }
	replace := func(at int) { bytes[at] = char() }
	delete := func(at int) { bytes = append(bytes[:at], bytes[at+1:]...) }
	insert := func(at int) { bytes = append(append(bytes[:at], char()), bytes[at:]...) }

	for i := 0; i < passes || avoid.Contains(string(bytes)); i++ {
		if len(bytes) > 0 {
			at := g.rnd.Intn(len(bytes))
			switch g.rnd.Intn(3) {
			case 0:
				replace(at)
			case 1:
				delete(at)
			case 2:
				insert(at)
			}
		} else {
			insert(0)
		}
	}
	return string(bytes), nil
}

// is returns a function that returns true if the value passed to the function
// matches the type of 'ty'.
func is(ty any) func(any) bool {
	rty := reflect.TypeOf(ty)
	return func(v any) bool {
		ty := reflect.TypeOf(v)
		return ty == rty || ty == reflect.PointerTo(rty)
	}
}

// isFirstIn returns true if v is the first element of the given slice.
func isFirstIn(v, slice any) bool {
	s := reflect.ValueOf(slice)
	count := s.Len()
	if count == 0 {
		return false
	}
	return s.Index(0).Interface() == v
}

// isFirstIn returns true if v is the last element of the given slice.
func isLastIn(v, slice any) bool {
	s := reflect.ValueOf(slice)
	count := s.Len()
	if count == 0 {
		return false
	}
	return s.Index(count-1).Interface() == v
}
