package main

import (
	"archive/zip"
	"bufio"
	"fmt"
	"io"
	"io/fs"
	"log"
	"os"
	"path/filepath"
)

const templatesPath = "../../templates"

// This program generates zz_filesystem_generated.go file containing byte array variable named TemplatesZip.
// The variable contains zip of "./templates" directory.
func main() {
	f, err := os.OpenFile("../../generate/zz_filesystem_generated.go", os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()

	srcOut := bufio.NewWriter(f)
	defer srcOut.Flush()

	_, err = fmt.Fprintf(srcOut, "// Code generated by go generate; DO NOT EDIT.\npackage generate\n\nvar TemplatesZip = []byte{")
	if err != nil {
		log.Fatal(err)
	}

	zipWriter := zip.NewWriter(newGoByteArrayWriter(srcOut))
	buff := make([]byte, 4*1024)
	err = filepath.Walk(templatesPath, func(path string, info fs.FileInfo, err error) error {
		if err != nil {
			return err
		}

		name, err := filepath.Rel(templatesPath, path)
		if err != nil {
			return err
		}
		if name == "." {
			return nil
		}
		name = filepath.ToSlash(name)
		if info.IsDir() {
			name = name + "/"
		}

		header := &zip.FileHeader{
			Name:   name,
			Method: zip.Deflate,
		}

		// Coercing permission to 755 for directories/executables and to 644 for non-executable files.
		// This is needed to ensure reproducible builds on machines with different values of `umask`.
		var mode fs.FileMode
		switch {
		case info.Mode()&fs.ModeSymlink != 0:
			mode = 0777 | fs.ModeSymlink
		case info.IsDir() || (info.Mode().Perm()&0111) != 0: // dir or executable
			mode = 0755
		case info.Mode()&fs.ModeType == 0: // regular file
			mode = 0644
		default:
			return fmt.Errorf("unsupported file type: %s", info.Mode().String())
		}
		header.SetMode(mode)

		w, err := zipWriter.CreateHeader(header)
		if err != nil {
			return err
		}

		switch {
		case info.Mode()&fs.ModeSymlink != 0:
			symlinkTarget, err := os.Readlink(path)
			if err != nil {
				return err
			}
			_, err = w.Write([]byte(filepath.ToSlash(symlinkTarget)))
			return err
		case info.Mode()&fs.ModeType == 0: // regular file
			f, err := os.Open(path)
			if err != nil {
				return err
			}
			defer f.Close()

			_, err = io.CopyBuffer(w, f, buff)
			return err
		default:
			return nil
		}
	})
	zipWriter.Close()
	if err != nil {
		log.Fatal(err)
	}

	_, err = fmt.Fprint(srcOut, "\n}\n")
	if err != nil {
		log.Fatal(err)
	}
}

// goByteArrayWriter dumps bytes as a Go integer hex literals separated by commas into underlying Writer.
// Each line of the output will be indented by a tab and each line will contain at most 32 integer literals.
// This is useful when generating Go array literals.
type goByteArrayWriter struct {
	i                 uint32
	w                 io.Writer
	hexDigitWithComma []byte
}

func newGoByteArrayWriter(w io.Writer) *goByteArrayWriter {
	return &goByteArrayWriter{
		i:                 0,
		w:                 w,
		hexDigitWithComma: []byte("0x00,"),
	}
}

var hexs = []byte("0123456789abcdef")
var space = []byte(" ")
var newLineAndTab = []byte("\n\t")

const bytesInLine = 32

func (g *goByteArrayWriter) Write(bs []byte) (written int, err error) {
	for _, b := range bs {
		if g.i == 0 {
			_, err = g.w.Write(newLineAndTab)
			if err != nil {
				return
			}
		} else {
			_, err = g.w.Write(space)
			if err != nil {
				return
			}
		}

		g.hexDigitWithComma[2] = hexs[b>>4]
		g.hexDigitWithComma[3] = hexs[b&0x0f]
		_, err = g.w.Write(g.hexDigitWithComma)
		if err != nil {
			return
		}

		if g.i == bytesInLine-1 {
			g.i = 0
		} else {
			g.i++
		}

		written += 1
	}

	return
}
