package parser

import (
	"bytes"
	"encoding/json"
	"esperframework/options"
	"log"
	"os"
	"path/filepath"
	"strings"
)

type DepGraphItem struct {
	TemplateKind    string          `json:"templateKind"` // file kind (ie. "page" | "layout" | "component")
	LayoutPaths     []string        `json:"layoutPaths"`
	Deps            []HeadBlockItem `json:"deps"`
	AdditionalDepth int             `json:"additionalDepth"`
}

type DepGraph map[string]DepGraphItem

func buildDepGraph(opts *options.Options, td TemplateData) []HeadBlockItem {
	// For testing
	if (*opts).CachePath == filepath.Join("../../../", "bin", "delete") {
		os.MkdirAll((*opts).CachePath, os.ModePerm)
	}

	graph := getDepGraph(opts)
	dgPath := filepath.Join((*opts).CachePath, "dep_graph.json")

	if entry, ok := graph[td.MetaData.MetaDataPath]; !ok {
		entry.Deps = graph[td.MetaData.MetaDataPath].Deps

		for _, hbi := range td.Head {
			entry.Deps = append(entry.Deps, hbi)
		}

		entry.AdditionalDepth = td.MetaData.AdditionalDepth
		entry.TemplateKind = td.MetaData.TemplateKind
		graph[td.MetaData.MetaDataPath] = entry
	}

	dg, err := json.Marshal(graph)
	if err != nil {
		log.Fatalf("\nFailed to marshal dependency graph into JSON\n%s\n", err)
	}

	err = os.WriteFile(dgPath, dg, os.ModePerm)
	if err != nil {
		log.Fatalf("\nFailed to write dependency graph at %s\n%s\n", dgPath, err)
	}

	return graph[td.MetaData.MetaDataPath].Deps
}

type list struct {
	head *graphNode
}

type graphNode struct {
	path     string
	parent   *graphNode
	children []*graphNode
}

func AssignLayoutsToDepGraphItems(opts *options.Options, dg *DepGraph) {
	appPagesDir := opts.AppDir
	if len(strings.Split(opts.AppDir, "/")) > 1 {
		_, appPagesDir, _ = strings.Cut(opts.AppDir, "/")
	}

	if len(appPagesDir) == 0 {
		log.Fatal("appPagesDir must be a string that with a length greater than zero")
	}

	filetree := list{
		head: &graphNode{
			path: appPagesDir,
			parent: &graphNode{
				path: "root",
			},
		},
	}

	for path := range *dg {
		ps := strings.Split(path, string(os.PathSeparator))

		if ps[1] != appPagesDir {
			continue
		}

		currNode := filetree.head

		// Start on index 2 since the first index is blank due to the way string cut works
		// and the second index is the head or root directory
		for i := 2; i < len(ps); i++ {

			idx := indexOf(currNode.children, ps[i])

			if idx != -1 {
				currNode = currNode.children[idx]
				continue
			}

			nn := &graphNode{
				path:   ps[i],
				parent: currNode,
			}

			currNode.children = append(currNode.children, nn)

			currNode = nn
		}
	}

	loMap := &map[string][]string{}

	buildLayoutMap(opts, dg, filetree.head, loMap)

	appendLayoutPaths(opts, dg, filetree.head, loMap)

	dgPath := filepath.Join((*opts).CachePath, "dep_graph.json")

	graph, err := json.Marshal(*dg)
	if err != nil {
		log.Fatalf("\nFailed to marshal dependency graph into JSON\n%s\n", err)
	}

	err = os.WriteFile(dgPath, graph, os.ModePerm)
	if err != nil {
		log.Fatalf("\nFailed to write dependency graph at %s\n%s\n", dgPath, err)
	}
}

func buildLayoutMap(opts *options.Options, dg *DepGraph, currNode *graphNode, loMap *map[string][]string) {
	for _, item := range currNode.children {
		// Handle directories
		if filepath.Ext(item.path) == "" {
			buildLayoutMap(opts, dg, item, loMap)
			continue
		}

		// Handle files
		fn, ln, ld := SplitLayoutNameAndLayoutDepFromFileName(item.path)

		cn := currNode
		lastLoop := false

		for {
			if cn.path == "root" {
				lastLoop = true
			}

			for _, cnItem := range cn.children {
				if filepath.Ext(cnItem.path) == "" {
					continue
				}

				if len((*loMap)[buildRelFilePath(item)]) == 0 {
					(*loMap)[buildRelFilePath(item)] = []string{}
				}

				cfn, cln, _ := SplitLayoutNameAndLayoutDepFromFileName(cnItem.path)

				if fn == "__layout" && cfn == "__layout" {
					if ld != "" && cln == ld {
						(*loMap)[buildRelFilePath(item)] = append([]string{buildRelFilePath(cnItem)}, (*loMap)[buildRelFilePath(item)]...)

						parLo := []string{}
						if loMapItem, ok := (*loMap)[buildRelFilePath(cnItem)]; ok {
							parLo = append(parLo, loMapItem...)
						}

						(*loMap)[buildRelFilePath(item)] = append(parLo, (*loMap)[buildRelFilePath(item)]...)

						goto OUTER
					}

					if cln == "" && ln == "" && item.path != cnItem.path {
						(*loMap)[buildRelFilePath(item)] = append([]string{buildRelFilePath(cnItem)}, (*loMap)[buildRelFilePath(item)]...)

						parLo := []string{}
						if loMapItem, ok := (*loMap)[buildRelFilePath(cnItem)]; ok {
							parLo = append(parLo, loMapItem...)
						}

						(*loMap)[buildRelFilePath(item)] = append(parLo, (*loMap)[buildRelFilePath(item)]...)

						goto OUTER
					}
				}
			}

			cn = cn.parent

			if lastLoop {
				break
			}
		}

	OUTER:
	}
}

func appendLayoutPaths(opts *options.Options, dg *DepGraph, currNode *graphNode, loMap *map[string][]string) {
	for _, item := range currNode.children {
		// Handle directories
		if filepath.Ext(item.path) == "" {
			appendLayoutPaths(opts, dg, item, loMap)
			continue
		}

		// Handle files
		_, ln, ld := SplitLayoutNameAndLayoutDepFromFileName(item.path)

		cn := currNode
		lastLoop := false

		for {
			if cn.path == "root" {
				lastLoop = true
			}

			for _, cnItem := range cn.children {
				if filepath.Ext(cnItem.path) == "" {
					continue
				}

				if entry, ok := (*dg)[buildRelFilePath(item)]; ok {
					if len(entry.LayoutPaths) == 0 {
						entry.LayoutPaths = []string{}
						(*dg)[buildRelFilePath(item)] = entry
					}
				}

				cfn, cln, _ := SplitLayoutNameAndLayoutDepFromFileName(cnItem.path)

				if cfn == "__layout" {
					if ld != "" && cln == ld {

						if entry, ok := (*dg)[buildRelFilePath(item)]; ok {
							entry.LayoutPaths = append([]string{buildRelFilePath(cnItem)}, entry.LayoutPaths...)

							parLo := []string{}
							if loMapItem, ok := (*loMap)[buildRelFilePath(cnItem)]; ok {
								parLo = append(parLo, loMapItem...)
							}

							entry.LayoutPaths = append(parLo, entry.LayoutPaths...)
							entry.LayoutPaths = dedupe(entry.LayoutPaths)

							(*dg)[buildRelFilePath(item)] = entry
						}
						goto OUTER
					}

					if cln == "" && ln == "" && item.path != cnItem.path {
						if entry, ok := (*dg)[buildRelFilePath(item)]; ok {
							entry.LayoutPaths = append([]string{buildRelFilePath(cnItem)}, entry.LayoutPaths...)

							parLo := []string{}
							if loMapItem, ok := (*loMap)[buildRelFilePath(cnItem)]; ok {
								parLo = append(parLo, loMapItem...)
							}

							entry.LayoutPaths = append(parLo, entry.LayoutPaths...)
							entry.LayoutPaths = dedupe(entry.LayoutPaths)

							(*dg)[buildRelFilePath(item)] = entry
						}
					}
				}
			}

			cn = cn.parent

			if lastLoop {
				break
			}
		}

	OUTER:
	}
}

func buildRelFilePath(currNode *graphNode) string {
	cn := currNode
	path := ""

	for {
		if cn.path == "root" {
			break
		}
		if path == "" {
			path = cn.path
		} else {
			path = cn.path + string(os.PathSeparator) + path
		}
		cn = cn.parent
	}

	return string(os.PathSeparator) + path
}

// Use on layouts                                     fileName, layoutName, layoutDep
func SplitLayoutNameAndLayoutDepFromFileName(path string) (string, string, string) {
	var fName bytes.Buffer
	var loName bytes.Buffer
	var loDep bytes.Buffer

	lexLOName := false
	lexLODep := false

	path = filepath.Base(path)

	for i := 0; i < len(path); i++ {
		if path[i] == '@' {
			lexLODep = true
			continue
		}

		if lexLODep {
			if path[i] == '.' {
				break
			}

			loDep.WriteByte(path[i])
			continue
		}

		if lexLOName {
			if path[i] == '.' {
				break
			}

			loName.WriteByte(path[i])
			continue
		}

		if path[i] == '.' {
			lexLOName = true
			continue
		}

		fName.WriteByte(path[i])
	}

	if loName.String() == filepath.Ext(path)[1:len(filepath.Ext(path))] {
		return fName.String(), "", ""
	}

	return fName.String(), loName.String(), loDep.String()
}

func getDepGraph(opts *options.Options) DepGraph {
	var graph = DepGraph{}

	dgPath := filepath.Join((*opts).CachePath, "dep_graph.json")

	// If the dep graph file exists read it into byte slice
	if _, err := os.Stat(dgPath); !os.IsNotExist(err) {
		var file []byte

		file, err = os.ReadFile(dgPath)
		if err != nil {
			log.Fatalf("\nFailed to read dependency graph file at path %s\n%s\n", dgPath, err)
		}

		err := json.Unmarshal(file, &graph)
		if err != nil {
			log.Fatalf("\nFailed to unmarshal dep graph JSON\n%s\n", err)
		}
	}

	return graph
}

func indexOf(slice []*graphNode, str string) int {
	for i, s := range slice {
		if s.path == str {
			return i
		}
	}
	return -1
}

func contains(s []string, str string) bool {
	for _, v := range s {
		if v == str {
			return true
		}
	}
	return false
}

func dedupe(strList []string) []string {
	list := []string{}
	for _, item := range strList {
		if contains(list, item) == false {
			list = append(list, item)
		}
	}
	return list
}
