package compiler

import (
	"bytes"
	"encoding/json"
	"esperframework/compiler/appshell"
	"esperframework/compiler/parser"
	"esperframework/options"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/karrick/godirwalk"
)

func generateHTML(opts *options.Options) {
	err := godirwalk.Walk((*opts).CachePath, &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)
			}

			if fi.IsDir() {
				return nil
			}

			if de.Name() == "__app.json" || de.Name() == "dep_graph.json" {
				return nil
			}

			file, err := ioutil.ReadFile(osPath)
			if err != nil {
				log.Fatalf("\nFailed to read file while generating HTML\n%s\n", err)
			}

			var pageData appshell.ASTree

			json.Unmarshal(file, &pageData)

			var html *bytes.Buffer = bytes.NewBuffer([]byte{})

			// DOCTYPE / open html
			html.Write([]byte(pageData.BeforeHead))

			// Head
			html.Write([]byte(pageData.HeadStart))
			injectRouter(opts, html)
			generateHead(html, pageData.HeadSlot.SlotItems)

			// Styles
			applyStyles(html, pageData.Styles.SlotItems)

			// Scripts
			applyScripts(html, pageData.Scripts.SlotItems)

			if opts.Dev == true {
				html.Write(getLiveReload(opts.Port))
			}

			html.Write([]byte(pageData.HeadEnd))

			// Body
			html.Write([]byte(pageData.BodyStart))
			generateBody(html, pageData.BodySlot.SlotItems)
			html.Write([]byte(pageData.BodyEnd))

			// Close html
			html.Write([]byte(pageData.AfterBody))

			// write to out path
			np := strings.Replace(osPath, (*opts).CachePath, (*opts).CacheClientPath, 1)
			outPath := strings.Replace(np, filepath.Ext(np), (*opts).Ext, 1)
			outDir := strings.Replace(outPath, string(os.PathSeparator)+filepath.Base(outPath), "", 1)

			if _, err := os.Stat(outDir); os.IsNotExist(err) {
				err = os.MkdirAll(outDir, os.ModePerm)
				if err != nil {
					log.Fatalf("\nFailed to make directory for output path while generating HTML at %s\n%s\n", outDir, err)
				}
			}

			os.WriteFile(outPath, html.Bytes(), os.ModePerm)

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

func generateHead(html *bytes.Buffer, items []appshell.SlotItemData) {
	layouts := []appshell.SlotItemData{}
	pages := []appshell.SlotItemData{}
	components := []appshell.SlotItemData{}

	for _, item := range items {
		if item.Typ == parser.TemplateKind[parser.Layout] {
			layouts = append(layouts, item)
			continue
		}

		if item.Typ == parser.TemplateKind[parser.Page] {
			pages = append(pages, item)
			continue
		}

		components = append(components, item)
	}

	for _, item := range layouts {
		html.Write([]byte(item.HTML.Open))
		if item.HTML.Close != "" {
			html.Write([]byte(item.HTML.Close))
		}
	}

	for _, item := range pages {
		html.Write([]byte(item.HTML.Open))
		if item.HTML.Close != "" {
			html.Write([]byte(item.HTML.Close))
		}
	}

	for _, item := range components {
		html.Write([]byte(item.HTML.Open))
		if item.HTML.Close != "" {
			html.Write([]byte(item.HTML.Close))
		}
	}
}

func applyStyles(html *bytes.Buffer, items []appshell.SlotItemData) {
	layouts := []appshell.SlotItemData{}
	pages := []appshell.SlotItemData{}
	components := []appshell.SlotItemData{}

	for _, item := range items {
		if item.Typ == parser.TemplateKind[parser.Layout] {
			layouts = append(layouts, item)
			continue
		}

		if item.Typ == parser.TemplateKind[parser.Page] {
			pages = append(pages, item)
			continue
		}

		components = append(components, item)
	}

	for _, item := range layouts {
		if item.HTML.Open != "" {
			html.WriteString("<style>")
			html.Write([]byte(item.HTML.Open))
			html.WriteString("</style>")
		}
	}

	for _, item := range pages {
		if item.HTML.Open != "" {
			html.WriteString("<style>")
			html.Write([]byte(item.HTML.Open))
			html.WriteString("</style>")
		}
	}

	for _, item := range components {
		if item.HTML.Open != "" {
			html.WriteString("<style>")
			html.Write([]byte(item.HTML.Open))
			html.WriteString("</style>")
		}
	}
}

func applyScripts(html *bytes.Buffer, items []appshell.SlotItemData) {
	layouts := []appshell.SlotItemData{}
	pages := []appshell.SlotItemData{}
	components := []appshell.SlotItemData{}

	for _, item := range items {
		if item.Typ == parser.TemplateKind[parser.Layout] {
			layouts = append(layouts, item)
			continue
		}

		if item.Typ == parser.TemplateKind[parser.Page] {
			pages = append(pages, item)
			continue
		}

		components = append(components, item)
	}

	for _, item := range layouts {
		if item.HTML.Open != "" {
			html.WriteString("<script type=\"module\" defer>")
			html.Write([]byte(item.HTML.Open))
			html.WriteString("</script>")
		}
	}

	for _, item := range pages {
		if item.HTML.Open != "" {
			html.WriteString("<script type=\"module\" defer>")
			html.Write([]byte(item.HTML.Open))
			html.WriteString("</script>")
		}
	}

	for _, item := range components {
		if item.HTML.Open != "" {
			html.WriteString("<script type=\"module\" defer>")
			html.Write([]byte(item.HTML.Open))
			html.WriteString("</script>")
		}
	}
}

var closingHTML = []string{}

func generateBody(html *bytes.Buffer, items []appshell.SlotItemData) {
	var componentMap = map[string]int{}

	// Generate component map
	for i, item := range items {
		if item.Typ == parser.TemplateKind[parser.Layout] || item.Typ == parser.TemplateKind[parser.Page] {
			continue
		}

		componentMap[fmt.Sprintf("${%s}", item.Typ)] = i
	}

	for i := 0; i < len(items); i++ {
		if items[i].Typ == parser.TemplateKind[parser.Layout] || items[i].Typ == parser.TemplateKind[parser.Page] {
			(*html).Write([]byte(replaceInlineComponents(items, items[i].HTML.Open, componentMap)))

			if items[i].HTML.Close != "" {
				closingHTML = append(closingHTML, items[i].HTML.Close)
			}
		}
	}

	for i := len(closingHTML) - 1; i >= 0; i-- {
		(*html).Write([]byte(replaceInlineComponents(items, closingHTML[i], componentMap)))

		if len(closingHTML) >= 0 {
			closingHTML = closingHTML[:len(closingHTML)-1]
		}
	}
}

func replaceInlineComponents(items []appshell.SlotItemData, html string, componentMap map[string]int) string {
	var str = ""

	i := 0
	for i < len(html) {
		if i == len(html)-1 {
			str += string(html[i])
			break
		}

		// Is not the start of an inline component
		if string(html[i]) != "$" && string(html[i+1]) != "{" {
			str += string(html[i])
			i++
			continue
		}

		// Get component key (ie. ${Links})
		j := i
		key := ""
		for j < len(html) {
			key += string(html[j])
			if string(html[j]) == "}" {
				break
			}
			j++
		}

		// Handle slotted inline components
		if len(strings.Split(key, "-")) > 1 {
			strSpl := strings.Split(key, "-")
			ogKey := key
			key = strSpl[0] + "}"

			if strSpl[1] == "open}" {
				str += replaceInlineComponents(items, items[componentMap[key]].HTML.Open, componentMap)
				i += len(ogKey)
				key = ""
				continue
			}

			if strSpl[1] == "close}" {
				str += replaceInlineComponents(items, items[componentMap[key]].HTML.Close, componentMap)
				i += len(ogKey)
				key = ""
				continue
			}
		}

		// Handle inline components
		if _, ok := componentMap[key]; ok {
			str += replaceInlineComponents(items, items[componentMap[key]].HTML.Open, componentMap)
			i += len(key)
			key = ""
			continue
		}

		i++
	}

	return str
}

func getLiveReload(port int) []byte {
	wd, err := os.Getwd()
	if err != nil {
		log.Fatalf("\nFailed to get working directory while getting the live reload file\n%s\n", err)
	}

	var buf bytes.Buffer

	buf.WriteString("<script type=\"module\" defer>")

	file, _ := ioutil.ReadFile(filepath.Join(wd, "../", "src", "dev", "livereload.tmpl.js"))

	for i := 0; i < len(file); i++ {
		// len(%PORT%) == 6
		if file[i] == '%' && string(file[i:i+6]) == "%PORT%" {
			buf.WriteString(strconv.Itoa(port))
			i += 5
			continue
		}

		buf.WriteByte(file[i])
	}

	buf.WriteString("</script>")

	return buf.Bytes()
}
