package genesis

import (
	"bytes"
	"crypto/sha256"
	"errors"
	"fmt"
	"io"
	"sort"
	"strings"
	"time"
)

var (
	ErrPackageNameRequired = errors.New("package name required")
)

// Asset represents an asset to generate code for.
type Asset struct {
	Name    string
	Data    []byte
	ModTime time.Time
}

// Hash returns the hex-encoded SHA256 hash of the asset data.
func (a *Asset) Hash() string {
	return fmt.Sprintf("%x", sha256.Sum256(a.Data))
}

// Encoder encodes asset data as an embedded Go file.
type Encoder struct {
	w       io.Writer           // underlying writer
	closed  bool                // if true, footer has been written.
	written bool                // if true, header has been written.
	names   map[string]struct{} // set asset names

	// Package name to write in header.
	Package string

	// Build tags written to header.
	Tags []string
}

// NewEncoder returns a new instance of Encoder attached to w.
func NewEncoder(w io.Writer) *Encoder {
	return &Encoder{
		w:     w,
		names: make(map[string]struct{}),
	}
}

// Encode encodes a single asset to the underlying writer.
func (enc *Encoder) Encode(asset *Asset) error {
	if err := enc.ensureHeaderWritten(); err != nil {
		return err
	}

	// Ensure asset hasn't already been encoded.
	if _, ok := enc.names[asset.Name]; ok {
		return fmt.Errorf("duplicate asset: %q", asset.Name)
	}
	enc.names[asset.Name] = struct{}{}

	// Calculate mod time parts.
	sec := asset.ModTime.UnixNano() / int64(time.Second)
	nsec := asset.ModTime.UnixNano() % int64(time.Second)

	var buf bytes.Buffer
	fmt.Fprintf(&buf, "	%q: &File{\n", asset.Name)
	fmt.Fprintf(&buf, "		name:    %q,\n", asset.Name)
	fmt.Fprintf(&buf, "		hash:    %q,\n", asset.Hash())
	fmt.Fprintf(&buf, "		modTime: time.Unix(%d, %d),\n", sec, nsec)
	fmt.Fprintf(&buf, "		data:    []byte(\"%s\"),\n", string(hex(asset.Data)))
	fmt.Fprintf(&buf, "	},\n")
	_, err := buf.WriteTo(enc.w)
	return err
}

// Close writes the rest of the file.
func (enc *Encoder) Close() error {
	if err := enc.ensureHeaderWritten(); err != nil {
		return err
	}

	// Close the assetMap variable and write asset names.
	if _, err := fmt.Fprint(enc.w, "}\n\n"); err != nil {
		return err
	} else if err := enc.writeAssetNames(); err != nil {
		return err
	}

	// Write generic types & functions.
	if err := enc.writeFileType(); err != nil {
		return err
	} else if err := enc.writeAssetFuncs(); err != nil {
		return err
	} else if err := enc.writeFileSystem(); err != nil {
		return err
	} else if err := enc.writeHashFuncs(); err != nil {
		return err
	}
	return nil
}

// ensureHeaderWritten writes the file header if it has not already been written.
func (enc *Encoder) ensureHeaderWritten() error {
	// Ensure we only write the header once.
	if enc.written {
		return nil
	}
	enc.written = true

	// Validate package name.
	pkg := strings.TrimSpace(enc.Package)
	if pkg == "" {
		return ErrPackageNameRequired
	}

	var buf bytes.Buffer
	fmt.Fprintln(&buf, "// Code generated by genesis.")
	fmt.Fprintln(&buf, "// DO NOT EDIT.")
	fmt.Fprintln(&buf, "")

	// Write build tags.
	for _, tag := range enc.Tags {
		tag = strings.TrimSpace(tag)
		if tag == "" {
			continue
		}
		fmt.Fprintf(&buf, "// +build %s\n\n", tag)
		fmt.Fprintln(&buf, "")
	}

	fmt.Fprintf(&buf, "package %s", pkg)
	fmt.Fprintln(&buf, "")

	// Write imports.
	fmt.Fprintln(&buf, "")
	fmt.Fprintln(&buf, `import (`)
	fmt.Fprintln(&buf, `	"bytes"`)
	fmt.Fprintln(&buf, `	"net/http"`)
	fmt.Fprintln(&buf, `	"os"`)
	fmt.Fprintln(&buf, `	"path"`)
	fmt.Fprintln(&buf, `	"regexp"`)
	fmt.Fprintln(&buf, `	"strings"`)
	fmt.Fprintln(&buf, `	"time"`)
	fmt.Fprintln(&buf, `)`)
	fmt.Fprintln(&buf, "")

	// Start asset map variable.
	if _, err := fmt.Fprintln(&buf, `var assetMap = map[string]*File{`); err != nil {
		return err
	}

	_, err := buf.WriteTo(enc.w)
	return err
}

// writeAssetNames writes all asset names to a variable named assetNames.
func (enc *Encoder) writeAssetNames() error {
	// Convert names to a sorted slice.
	names := make([]string, 0, len(enc.names))
	for name := range enc.names {
		names = append(names, name)
	}
	sort.Strings(names)

	// Write names to a slice variable.
	var buf bytes.Buffer
	fmt.Fprint(&buf, `var assetNames = []string{`+"\n")
	for _, name := range names {
		fmt.Fprintf(&buf, "	%q,\n", name)
	}
	fmt.Fprint(&buf, "}\n\n")

	_, err := buf.WriteTo(enc.w)
	return err
}

func (enc *Encoder) writeFileType() error {
	var buf bytes.Buffer
	fmt.Fprintln(&buf, `// File represents a single embedded asset file.`)
	fmt.Fprintln(&buf, `type File struct {`)
	fmt.Fprintln(&buf, `	name    string`)
	fmt.Fprintln(&buf, `	hash    string`)
	fmt.Fprintln(&buf, `	modTime time.Time`)
	fmt.Fprintln(&buf, `	data    []byte`)
	fmt.Fprintln(&buf, `}`)
	fmt.Fprintln(&buf, ``)
	fmt.Fprintln(&buf, `// Name returns the full path of the file.`)
	fmt.Fprintln(&buf, `func (f *File) Name() string { return f.name }`)
	fmt.Fprintln(&buf, ``)
	fmt.Fprintln(&buf, `// Hash returns the SHA256 hash of the file's data.`)
	fmt.Fprintln(&buf, `func (f *File) Hash() string { return f.hash }`)
	fmt.Fprintln(&buf, ``)
	fmt.Fprintln(&buf, `// ModTime returns the last modified date of the file when it was generated.`)
	fmt.Fprintln(&buf, `func (f *File) ModTime() time.Time { return f.modTime }`)
	fmt.Fprintln(&buf, ``)
	fmt.Fprintln(&buf, `// Data returns the raw embedded data for the file.`)
	fmt.Fprintln(&buf, `func (f *File) Data() []byte { return f.data }`)
	fmt.Fprintln(&buf, ``)
	_, err := buf.WriteTo(enc.w)
	return err
}

func (enc *Encoder) writeAssetFuncs() error {
	var buf bytes.Buffer
	fmt.Fprintln(&buf, `// Asset returns the raw data given an embedded filename.`)
	fmt.Fprintln(&buf, `// Returns nil if the asset cannot be found.`)
	fmt.Fprintln(&buf, `func Asset(name string) []byte {`)
	fmt.Fprintln(&buf, `	if f := AssetFile(name); f != nil {`)
	fmt.Fprintln(&buf, `		return f.Data()`)
	fmt.Fprintln(&buf, `	}`)
	fmt.Fprintln(&buf, `	return nil`)
	fmt.Fprintln(&buf, `}`)
	fmt.Fprintln(&buf, ``)
	fmt.Fprintln(&buf, `// AssetFile returns the File object given an embedded filename.`)
	fmt.Fprintln(&buf, `// Returns nil if the asset cannot be found.`)
	fmt.Fprintln(&buf, `func AssetFile(name string) *File {`)
	fmt.Fprintln(&buf, `	if f := assetMap[name]; f != nil {`)
	fmt.Fprintln(&buf, `		return f`)
	fmt.Fprintln(&buf, `	}`)
	fmt.Fprintln(&buf, `	file, hash := SplitNameHash(name)`)
	fmt.Fprintln(&buf, `	if f := assetMap[file]; f != nil && strings.HasPrefix(f.hash, hash) {`)
	fmt.Fprintln(&buf, `		return f`)
	fmt.Fprintln(&buf, `	}`)
	fmt.Fprintln(&buf, `	return nil`)
	fmt.Fprintln(&buf, `}`)
	fmt.Fprintln(&buf, ``)
	fmt.Fprintln(&buf, `// AssetNames returns a sorted list of all embedded asset filenames.`)
	fmt.Fprintln(&buf, `func AssetNames() []string {`)
	fmt.Fprintln(&buf, `	return assetNames`)
	fmt.Fprintln(&buf, `}`)
	fmt.Fprintln(&buf, ``)
	fmt.Fprintln(&buf, `// AssetNameWithHash returns the asset name with a short hash before the extension.`)
	fmt.Fprintln(&buf, `// Returns name if the asset cannot be found.`)
	fmt.Fprintln(&buf, `func AssetNameWithHash(name string) string {`)
	fmt.Fprintln(&buf, `	if f := AssetFile(name); f != nil {`)
	fmt.Fprintln(&buf, `		return JoinNameHash(f.Name(), f.Hash())`)
	fmt.Fprintln(&buf, `	}`)
	fmt.Fprintln(&buf, `	return name`)
	fmt.Fprintln(&buf, `}`)
	fmt.Fprintln(&buf, ``)

	_, err := buf.WriteTo(enc.w)
	return err
}

func (enc *Encoder) writeFileSystem() error {
	var buf bytes.Buffer
	fmt.Fprintln(&buf, `// FileSystem returns a http.FileSystem implementation of the embedded files.`)
	fmt.Fprintln(&buf, `// This can be used with http.FileServer().`)
	fmt.Fprintln(&buf, `func FileSystem() http.FileSystem { return &httpFileSystem{} }`)
	fmt.Fprintln(&buf, ``)
	fmt.Fprintln(&buf, `type httpFileSystem struct{}`)
	fmt.Fprintln(&buf, ``)
	fmt.Fprintln(&buf, `func (fs *httpFileSystem) Open(name string) (http.File, error) {`)
	fmt.Fprintln(&buf, `	f := AssetFile(name)`)
	fmt.Fprintln(&buf, `	if f == nil {`)
	fmt.Fprintln(&buf, `		return nil, &os.PathError{Path: "/" + name, Err: os.ErrNotExist}`)
	fmt.Fprintln(&buf, `	}`)
	fmt.Fprintln(&buf, `	return newHTTPFile(f), nil`)
	fmt.Fprintln(&buf, `}`)
	fmt.Fprintln(&buf, ``)

	fmt.Fprintln(&buf, `// FileServer returns a handler that supports accessing assets with embedded hashes.`)
	fmt.Fprintln(&buf, `func FileServer() http.Handler {`)
	fmt.Fprintln(&buf, `	return &httpFileServer{}`)
	fmt.Fprintln(&buf, `}`)
	fmt.Fprintln(&buf, ``)
	fmt.Fprintln(&buf, `type httpFileServer struct{}`)
	fmt.Fprintln(&buf, ``)
	fmt.Fprintln(&buf, `func (h *httpFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {`)
	fmt.Fprintln(&buf, `	name := r.URL.Path`)
	fmt.Fprintln(&buf, `	if !strings.HasPrefix(name, "/") {`)
	fmt.Fprintln(&buf, `		name = "/" + name`)
	fmt.Fprintln(&buf, `		r.URL.Path = name`)
	fmt.Fprintln(&buf, `	}`)
	fmt.Fprintln(&buf, ``)
	fmt.Fprintln(&buf, `	f := AssetFile(path.Clean(name))`)
	fmt.Fprintln(&buf, `	if f == nil {`)
	fmt.Fprintln(&buf, `		http.Error(w, "404 page not found", http.StatusNotFound)`)
	fmt.Fprintln(&buf, `		return`)
	fmt.Fprintln(&buf, `	}`)
	fmt.Fprintln(&buf, ``)
	fmt.Fprintln(&buf, `	if HasNameHash(name) {`)
	fmt.Fprintln(&buf, `		w.Header().Set("Cache-Control", "max-age=31536000")`)
	fmt.Fprintln(&buf, `	}`)
	fmt.Fprintln(&buf, `	http.ServeContent(w, r, f.Name(), f.ModTime(), newHTTPFile(f))`)
	fmt.Fprintln(&buf, `}`)
	fmt.Fprintln(&buf, ``)

	fmt.Fprintln(&buf, `func newHTTPFile(f *File) *httpFile {`)
	fmt.Fprintln(&buf, `	return &httpFile{File: f, Reader: bytes.NewReader(f.data)}`)
	fmt.Fprintln(&buf, `}`)
	fmt.Fprintln(&buf, ``)
	fmt.Fprintln(&buf, `type httpFile struct {`)
	fmt.Fprintln(&buf, `	*File`)
	fmt.Fprintln(&buf, `	*bytes.Reader`)
	fmt.Fprintln(&buf, `}`)
	fmt.Fprintln(&buf, ``)
	fmt.Fprintln(&buf, `func (f *httpFile) Close() error               { return nil }`)
	fmt.Fprintln(&buf, `func (f *httpFile) Stat() (os.FileInfo, error) { return f, nil }`)
	fmt.Fprintln(&buf, `func (f *httpFile) Size() int64                { return int64(len(f.File.Data())) }`)
	fmt.Fprintln(&buf, `func (f *httpFile) Mode() os.FileMode          { return 0444 }`)
	fmt.Fprintln(&buf, `func (f *httpFile) ModTime() time.Time         { return time.Time{} }`)
	fmt.Fprintln(&buf, `func (f *httpFile) IsDir() bool                { return false }`)
	fmt.Fprintln(&buf, `func (f *httpFile) Sys() interface{}           { return nil }`)
	fmt.Fprintln(&buf, `func (f *httpFile) Readdir(count int) ([]os.FileInfo, error) {`)
	fmt.Fprintln(&buf, `	return nil, &os.PathError{Path: "/" + f.name, Err: os.ErrPermission}`)
	fmt.Fprintln(&buf, `}`)
	fmt.Fprintln(&buf, ``)

	_, err := buf.WriteTo(enc.w)
	return err
}

func (enc *Encoder) writeHashFuncs() error {
	var buf bytes.Buffer
	fmt.Fprintln(&buf, `// JoinNameHash returns a filename with hash embedded before the extension.`)
	fmt.Fprintln(&buf, `func JoinNameHash(name, hash string) string {`)
	fmt.Fprintln(&buf, `	return strings.TrimSuffix(name, path.Ext(name)) + "-" + hash + path.Ext(name)`)
	fmt.Fprintln(&buf, `}`)
	fmt.Fprintln(&buf, ``)
	fmt.Fprintln(&buf, `// SplitNameHash separates name into its original filename and hash parts.`)
	fmt.Fprintln(&buf, `func SplitNameHash(name string) (file, hash string) {`)
	fmt.Fprintln(&buf, `	ext := path.Ext(name)`)
	fmt.Fprintln(&buf, `	base := strings.TrimSuffix(name, ext)`)
	fmt.Fprintln(&buf, `	file = hashSuffixRegex.ReplaceAllLiteralString(base, "") + path.Ext(name)`)
	fmt.Fprintln(&buf, `	if s := hashSuffixRegex.FindString(base); s != "" {`)
	fmt.Fprintln(&buf, `		hash = s[1:]`)
	fmt.Fprintln(&buf, `	}`)
	fmt.Fprintln(&buf, `	return file, hash`)
	fmt.Fprintln(&buf, `}`)
	fmt.Fprintln(&buf, ``)
	fmt.Fprintln(&buf, `// HasNameHash returns true if name contains an embedded hash.`)
	fmt.Fprintln(&buf, `func HasNameHash(name string) bool {`)
	fmt.Fprintln(&buf, `	return hashSuffixRegex.MatchString(strings.TrimSuffix(name, path.Ext(name)))`)
	fmt.Fprintln(&buf, `}`)
	fmt.Fprintln(&buf, ``)

	fmt.Fprintln(&buf, `var hashSuffixRegex = regexp.MustCompile("-[0-9a-f]{7,}$")`)

	_, err := buf.WriteTo(enc.w)
	return err
}

// hex returns a byte slice represented as a \x00 formatted string.
func hex(data []byte) string {
	buf := make([]byte, 4*len(data))
	const digits = "0123456789abcdef"
	for i, b := range data {
		buf[i*4] = '\\'
		buf[(i*4)+1] = 'x'
		buf[(i*4)+2] = digits[b>>4]
		buf[(i*4)+3] = digits[b&0x0F]
	}
	return string(buf)
}
