package main

import (
	"bytes"
	"esperframework/cliFlags"
	"esperframework/compiler"
	"esperframework/compiler/appshell"
	"esperframework/options"
	"esperframework/server"
	"esperframework/utils"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"runtime/debug"
	"strings"
	"time"
	"unicode"

	"github.com/karrick/godirwalk"
)

func main() {
	start := time.Now()
	defer utils.PrintElapsed(start)
	debug.SetGCPercent(6480)
	log.SetFlags(0)

	cliFlags.ParseFlags()

	opts := options.Get()

	setup(opts)

	actionMeta := options.GetActionMeta()

	compiler.Compile(opts, actionMeta)

	server.Build(opts)
}

func setup(opts *options.Options) {
	cleanBuildArtifacts(opts)

	// Create build/cache directory
	if _, err := os.Stat((*opts).CachePath); os.IsNotExist(err) {
		err := os.MkdirAll((*opts).CachePath, os.ModePerm)
		if err != nil {
			log.Fatalf("\nFailed to make directory for cache path at %s\n%s\n", (*opts).CachePath, err)
		}
	}

	// Handle go.mod and go.sum
	handelGoModFiles(opts)

	// Parse appshell
	var l = &appshell.ASLexer{}
	l.Pos = 0
	l.Options = opts
	appshell.Parse(l, false)

	publicPath := filepath.Join((*opts).Root, "src", (*opts).PublicPath)

	// If public dir doesn't exist, bail
	if _, err := os.Stat(publicPath); os.IsNotExist(err) {
		return
	}

	// Copy public dir to build dir
	err := godirwalk.Walk(publicPath, &godirwalk.Options{
		Callback: func(osPath string, de *godirwalk.Dirent) error {
			fi, err := os.Stat(osPath)
			if err != nil {
				log.Fatalf("\nFailed to stat file at %s\n%s\n", osPath, err)
			}

			dest := filepath.Join(opts.CacheClientPath, strings.Split(osPath, publicPath)[1])

			if fi.IsDir() {
				if _, err := os.Stat(dest); os.IsNotExist(err) {
					err := os.MkdirAll(dest, os.ModePerm)
					if err != nil {
						log.Fatalf("\nFailed to make directory at %s\n%s\n", dest, err)
					}
				}
				return nil
			}

			utils.Copy(osPath, dest)

			return nil
		},
		Unsorted: true,
	})
	if err != nil {
		log.Fatalf("\nError: Setup function failed with error\n%s\n", err)
	}
}

func cleanBuildArtifacts(opts *options.Options) {
	if _, err := os.Stat((*opts).OutPath); os.IsNotExist(err) {
		err = os.MkdirAll((*opts).OutPath, os.ModePerm)
		if err != nil {
			log.Fatalf("\nFailed to make directory for output path while cleaning build artifacts at %s\n%s\n", (*opts).OutPath, err)
		}
		return
	}

	err := godirwalk.Walk((*opts).OutPath, &godirwalk.Options{
		Callback: func(osPath string, de *godirwalk.Dirent) error {
			if osPath == (*opts).OutPath {
				return nil
			}

			err := os.RemoveAll(osPath)
			if err != nil {
				log.Fatalf("\nFailed to remove directory %s\n%s\n", osPath, err)
			}

			return godirwalk.SkipThis
		},
		Unsorted: true,
	})
	if err != nil {
		log.Fatalf("\nError: Clean build artifacts failed with error\n%s\n", err)
	}
}

func handelGoModFiles(opts *options.Options) {
	if _, exists := os.Stat((*opts).CacheClientPath); os.IsNotExist(exists) {
		os.MkdirAll((*opts).CacheClientPath, os.ModePerm)
	}

	// Check for go.mod
	file := []byte{}
	var err error

	if _, exists := os.Stat(filepath.Join((*opts).Root, "go.mod")); os.IsNotExist(exists) {
		// If go.mod is not in project root
		wd, err := os.Getwd()
		if err != nil {
			log.Fatalf("Failed to get working directory while handling go mod file")
		}

		file, err = ioutil.ReadFile(filepath.Join(wd, "../", "src", "templates", "go.tmpl.mod"))
		if err != nil {
			log.Fatalf("\nFailed to open file at %s while handling go mod files\n%s\n", filepath.Join((*opts).Root, "go.mod"), err)
		}
	} else {
		// If go.mod is in project root
		file, err = ioutil.ReadFile(filepath.Join((*opts).Root, "go.mod"))
		if err != nil {
			log.Fatalf("\nFailed to open file at %s while handling go mod files\n%s\n", filepath.Join((*opts).Root, "go.mod"), err)
		}
	}

	// Copy go.mod to server dir
	var buf = bytes.NewBuffer([]byte{})

	replaceGoModName(buf, file)

	dest := filepath.Join(opts.OutPath, "go.mod")

	err = os.WriteFile(dest, buf.Bytes(), os.ModePerm)
	if err != nil {
		log.Fatalf("\nFailed to write go.mod to while handling go mod files %s\n%s\n", dest, err)
	}

	// Check for go.sum
	// Copy go.sum to server dir
	if _, exists := os.Stat(filepath.Join((*opts).Root, "go.sum")); os.IsNotExist(exists) {
		// If go.sum is not in project root
		wd, err := os.Getwd()
		if err != nil {
			log.Fatalf("Failed to get working directory while handling go mod file")
		}

		utils.Copy(filepath.Join(wd, "../", "src", "templates", "go.tmpl.sum"), filepath.Join(opts.OutPath, "go.sum"))
	} else {
		// If go.sum is in project root
		utils.Copy(filepath.Join((*opts).Root, "go.sum"), filepath.Join(opts.OutPath, "go.sum"))
	}
}

// Replace module name in go.mod to avoid tooling collisions
func replaceGoModName(buf *bytes.Buffer, file []byte) {
	for i := 0; i < len(file); i++ {

		breakModRename := false
		if file[i] == 'g' && len(file)+3 < len(file) && string(file[i:3]) == "go " {
			breakModRename = true
		}

		length := 0
		// len(module) == 6
		if breakModRename == false && file[i] == 'm' && i+6 <= len(file) && string(file[i:i+6]) == "module" {
			buf.WriteString("module ")
			i += 5

			for j := i; j < len(file); j++ {
				if unicode.IsSpace(rune(file[i])) {
					buf.WriteByte(file[i])
					i++
					continue
				}
				i += 2
				break
			}

			for j := i; j < len(file); j++ {
				if unicode.IsSpace(rune(file[j])) {
					// write new module name and break to outer loop
					buf.WriteString(utils.ComputeHash(2, 2))
					break
				}

				buf.WriteByte(file[j])
				length++
			}
		}

		if length > 0 {
			i += length
			length = 0
		}

		buf.WriteByte(file[i])
	}
}
