// Copyright 2020 The Go 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 staticdata

import (
	"path"
	"sort"
	"strings"

	"cmd/compile/internal/base"
	"cmd/compile/internal/ir"
	"cmd/compile/internal/objw"
	"cmd/compile/internal/types"
	"cmd/internal/obj"
)

const (
	embedUnknown = iota
	embedBytes
	embedString
	embedFiles
)

func embedFileList(v *ir.Name, kind int) []string {
	// Build list of files to store.
	have := make(map[string]bool)
	var list []string
	for _, e := range *v.Embed {
		for _, pattern := range e.Patterns {
			files, ok := base.Flag.Cfg.Embed.Patterns[pattern]
			if !ok {
				base.ErrorfAt(e.Pos, "invalid go:embed: build system did not map pattern: %s", pattern)
			}
			for _, file := range files {
				if base.Flag.Cfg.Embed.Files[file] == "" {
					base.ErrorfAt(e.Pos, "invalid go:embed: build system did not map file: %s", file)
					continue
				}
				if !have[file] {
					have[file] = true
					list = append(list, file)
				}
				if kind == embedFiles {
					for dir := path.Dir(file); dir != "." && !have[dir]; dir = path.Dir(dir) {
						have[dir] = true
						list = append(list, dir+"/")
					}
				}
			}
		}
	}
	sort.Slice(list, func(i, j int) bool {
		return embedFileLess(list[i], list[j])
	})

	if kind == embedString || kind == embedBytes {
		if len(list) > 1 {
			base.ErrorfAt(v.Pos(), "invalid go:embed: multiple files for type %v", v.Type())
			return nil
		}
	}

	return list
}

// embedKind determines the kind of embedding variable.
func embedKind(typ *types.Type) int {
	if typ.Sym() != nil && typ.Sym().Name == "FS" && (typ.Sym().Pkg.Path == "embed" || (typ.Sym().Pkg == types.LocalPkg && base.Ctxt.Pkgpath == "embed")) {
		return embedFiles
	}
	if typ.Kind() == types.TSTRING {
		return embedString
	}
	if typ.Sym() == nil && typ.IsSlice() && typ.Elem().Kind() == types.TUINT8 {
		return embedBytes
	}
	return embedUnknown
}

func embedFileNameSplit(name string) (dir, elem string, isDir bool) {
	if name[len(name)-1] == '/' {
		isDir = true
		name = name[:len(name)-1]
	}
	i := len(name) - 1
	for i >= 0 && name[i] != '/' {
		i--
	}
	if i < 0 {
		return ".", name, isDir
	}
	return name[:i], name[i+1:], isDir
}

// embedFileLess implements the sort order for a list of embedded files.
// See the comment inside ../../../../embed/embed.go's Files struct for rationale.
func embedFileLess(x, y string) bool {
	xdir, xelem, _ := embedFileNameSplit(x)
	ydir, yelem, _ := embedFileNameSplit(y)
	return xdir < ydir || xdir == ydir && xelem < yelem
}

// WriteEmbed emits the init data for a //go:embed variable,
// which is either a string, a []byte, or an embed.FS.
func WriteEmbed(v *ir.Name) {
	// TODO(mdempsky): User errors should be reported by the frontend.

	commentPos := (*v.Embed)[0].Pos
	if !types.AllowsGoVersion(types.LocalPkg, 1, 16) {
		prevPos := base.Pos
		base.Pos = commentPos
		base.ErrorfVers("go1.16", "go:embed")
		base.Pos = prevPos
		return
	}
	if base.Flag.Cfg.Embed.Patterns == nil {
		base.ErrorfAt(commentPos, "invalid go:embed: build system did not supply embed configuration")
		return
	}
	kind := embedKind(v.Type())
	if kind == embedUnknown {
		base.ErrorfAt(v.Pos(), "go:embed cannot apply to var of type %v", v.Type())
		return
	}

	files := embedFileList(v, kind)
	switch kind {
	case embedString, embedBytes:
		file := files[0]
		fsym, size, err := fileStringSym(v.Pos(), base.Flag.Cfg.Embed.Files[file], kind == embedString, nil)
		if err != nil {
			base.ErrorfAt(v.Pos(), "embed %s: %v", file, err)
		}
		sym := v.Linksym()
		off := 0
		off = objw.SymPtr(sym, off, fsym, 0)       // data string
		off = objw.Uintptr(sym, off, uint64(size)) // len
		if kind == embedBytes {
			objw.Uintptr(sym, off, uint64(size)) // cap for slice
		}

	case embedFiles:
		slicedata := base.Ctxt.Lookup(`"".` + v.Sym().Name + `.files`)
		off := 0
		// []files pointed at by Files
		off = objw.SymPtr(slicedata, off, slicedata, 3*types.PtrSize) // []file, pointing just past slice
		off = objw.Uintptr(slicedata, off, uint64(len(files)))
		off = objw.Uintptr(slicedata, off, uint64(len(files)))

		// embed/embed.go type file is:
		//	name string
		//	data string
		//	hash [16]byte
		// Emit one of these per file in the set.
		const hashSize = 16
		hash := make([]byte, hashSize)
		for _, file := range files {
			off = objw.SymPtr(slicedata, off, StringSym(v.Pos(), file), 0) // file string
			off = objw.Uintptr(slicedata, off, uint64(len(file)))
			if strings.HasSuffix(file, "/") {
				// entry for directory - no data
				off = objw.Uintptr(slicedata, off, 0)
				off = objw.Uintptr(slicedata, off, 0)
				off += hashSize
			} else {
				fsym, size, err := fileStringSym(v.Pos(), base.Flag.Cfg.Embed.Files[file], true, hash)
				if err != nil {
					base.ErrorfAt(v.Pos(), "embed %s: %v", file, err)
				}
				off = objw.SymPtr(slicedata, off, fsym, 0) // data string
				off = objw.Uintptr(slicedata, off, uint64(size))
				off = int(slicedata.WriteBytes(base.Ctxt, int64(off), hash))
			}
		}
		objw.Global(slicedata, int32(off), obj.RODATA|obj.LOCAL)
		sym := v.Linksym()
		objw.SymPtr(sym, 0, slicedata, 0)
	}
}
