package compiler

import (
	"bytes"
	"esperframework/compiler/appshell"
	"esperframework/compiler/parser"
	"esperframework/options"
	"esperframework/utils"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"reflect"
	"strings"
)

type headItemMeta struct {
	path          string
	headBlockItem parser.HeadBlockItem
}

func aggregateHeadItems(opts *options.Options, td *parser.TemplateData, dg *parser.DepGraph, templateHeadData *[]appshell.SlotItemData, templateImportData *[]appshell.ImportData, templatesSeen *map[string][]appshell.SlotItemData) {
	if _, ok := (*templatesSeen)[td.MetaData.MetaDataPath]; ok {
		return
	}

	for _, path := range (*dg)[td.MetaData.MetaDataPath].LayoutPaths {
		if _, ok := (*templatesSeen)[path]; ok {
			continue
		}

		td := getTemplateData(opts, path)
		aggregateHeadItems(opts, &td, dg, templateHeadData, templateImportData, templatesSeen)
	}

	for i, hi := range td.Head {
		if hi.Kind.Value == "import" {
			depth := ""
			for i := 0; i < td.MetaData.AdditionalDepth; i++ {
				depth += "../"
			}

			*templateImportData = append(*templateImportData, appshell.ImportData{
				Parent: td.MetaData.Path,
				Path:   hi.Path.RelPath,
				Name:   hi.Attrs["name"],
			})

			src := filepath.Join(strings.Split(td.MetaData.MetaDataPath, filepath.Base(td.MetaData.MetaDataPath))[0], depth+strings.Split(hi.Path.RelPath, filepath.Ext(hi.Path.RelPath))[0]+".json")

			td := getTemplateData(opts, src)
			aggregateHeadItems(opts, &td, dg, templateHeadData, templateImportData, templatesSeen)
			continue
		}

		lastItem := false
		if i == len(td.Head)-1 {
			lastItem = true
		}

		relPath := filepath.Join(strings.Split(td.MetaData.Path, filepath.Base(td.MetaData.Path))[0], hi.Path.RelPath)

		src := filepath.Join(opts.Root, relPath)
		fi, err := os.Stat(src)
		if err != nil {
			log.Fatalf("\nFailed to stat file source at while appending head items %s\n%s\n", src, err)
		}

		if filepath.Ext(relPath) == opts.Ext || fi.Mode().IsDir() {
			appendHeadItems(opts, td, templateHeadData, &hi, lastItem)
			continue
		}

		nestedDirs := strings.Split(strings.Split(src, "src")[1], filepath.Base(src))[0]
		destDir := filepath.Join(opts.CacheClientPath, nestedDirs)
		dest := filepath.Join(destDir, hi.Path.HashedName)

		hi.Path.AbsPath = strings.Split(dest, opts.CacheClientPath)[1]

		fi, err = os.Stat(destDir)
		if os.IsNotExist(err) {
			err = os.MkdirAll(destDir, os.ModePerm)
			if err != nil {
				log.Fatalf("\nFailed to make styles dir at %s\n%s\n", destDir, err)
			}
		}

		// TODO: This is where to transform imported files (ie. .scss -> .css)
		// Files that are transformed would not be copied. They would be transformed and rewritten as their new file type.
		ext := filepath.Ext(dest)
		contains := false
		for _, x := range []string{".css", ".js"} {
			if ext == x {
				contains = true
			}
		}

		if contains == false {
			log.Fatalf("%s is currently unsupported file type", ext)
		}

		utils.Copy(src, dest)
		appendHeadItems(opts, td, templateHeadData, &hi, lastItem)
	}

	(*templatesSeen)[td.MetaData.MetaDataPath] = *templateHeadData
}

func appendHeadItems(opts *options.Options, td *parser.TemplateData, templateHeadData *[]appshell.SlotItemData, hi *parser.HeadBlockItem, lastItem bool) {
	item := appshell.SlotItemData{}
	item.Path = td.MetaData.MetaDataPath

	if td.MetaData.TemplateKind == parser.TemplateKind[parser.Layout] {
		item.Typ = parser.TemplateKind[parser.Layout]
	} else {
		item.Typ = parser.TemplateKind[parser.Page]
	}

	if len(item.Boundaries) == 0 {
		item.Boundaries = make([]string, 2)
	}

	hash := "WF" + utils.ComputeHash(2, 4)

	if len(hi.Attrs) == 0 {
		(*hi).Attrs = map[string]string{}
	} else {
		for key := range (*hi).Attrs {
			if key[:2] == "WF" {
				hash = key
			}
		}
	}

	idx := indexOf(*templateHeadData, "Path", td.MetaData.MetaDataPath)

	// Handle first item (Generate hash attr)
	if idx == -1 {
		item.Boundaries[0] = "head " + hash
		hi.Attrs[hash] = ""

		item.HTML.Open = printHeadItem(opts, hi)
		*templateHeadData = append(*templateHeadData, item)
		return
	}

	// Handle last item (Generate hash attr)
	if lastItem {
		(*templateHeadData)[idx].Boundaries[1] = "head " + hash
		hi.Attrs[hash] = ""
	}

	(*templateHeadData)[idx].HTML.Open += printHeadItem(opts, hi)
}

func printHeadItem(opts *options.Options, hi *parser.HeadBlockItem) string {
	var buf bytes.Buffer
	buf.WriteString("<")
	buf.WriteString(hi.Kind.Name)

	if hi.Kind.RelType != "" {
		buf.WriteString(" " + hi.Kind.RelType)

		if hi.Kind.Value != "" {
			buf.WriteString(fmt.Sprintf("=\"%s\"", hi.Kind.Value))
		}
	}

	if hi.Path.PathType != "" {
		buf.WriteString(" " + hi.Path.PathType)

		if hi.Path.RelPath != "" {
			buf.WriteString(fmt.Sprintf("=\"%s\"", hi.Path.AbsPath))
		}
	}

	buf.WriteString(printAttrs(hi.Attrs))

	buf.WriteString(">")

	if hi.Value != "" {
		buf.WriteString(hi.Value)
		buf.WriteString(fmt.Sprintf("</%s>", hi.Kind.Name))
	} else {
		switch hi.Kind.Name {
		case "script":
			buf.WriteString("</script>")
		case "style":
			buf.WriteString("</style>")
		}
	}

	return buf.String()
}

func printAttrs(attrs map[string]string) string {
	var buf bytes.Buffer

	i := 0
	for k, v := range attrs {
		if i == 0 && i == len(attrs)-1 {
			buf.WriteString(
				printAttr(k, v, true, false),
			)
			break
		}

		if i == 0 {
			buf.WriteString(
				printAttr(k, v, true, false),
			)
			continue
		}

		if i == len(attrs)-1 {
			buf.WriteString(
				printAttr(k, v, false, false),
			)
			break
		}

		buf.WriteString(
			printAttr(k, v, false, true),
		)

		i++
	}

	return buf.String()
}

func printAttr(k, v string, openingSpace, trailingSpace bool) string {
	if openingSpace {
		if v == "" {
			return " " + k
		}
		return fmt.Sprintf(" %s=\"%s\"", k, v)
	}
	if trailingSpace {
		if v == "" {
			return k + " "
		}
		return fmt.Sprintf("%s=\"%s\" ", k, v)
	}
	if v == "" {
		return k
	}
	return fmt.Sprintf("%s=\"%s\"", k, v)
}

func indexOf(sl []appshell.SlotItemData, key, value string) int {
	for i, item := range sl {
		r := reflect.ValueOf(item)
		f := reflect.Indirect(r).FieldByName(key)

		if f.String() == value {
			return i
		}
	}
	return -1
}
